Swiftgram/submodules/TelegramCallsUI/Sources/VoiceChatController.swift

6866 lines
394 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import TelegramVoip
import TelegramAudio
import AccountContext
import Postbox
import TelegramCore
import MergeLists
import ItemListUI
import AppBundle
import ContextUI
import ShareController
import DeleteChatPeerActionSheetItem
import UndoUI
import AlertUI
import PresentationDataUtils
import DirectionalPanGesture
import PeerInfoUI
import AvatarNode
import TooltipUI
import LegacyUI
import LegacyComponents
import LegacyMediaPickerUI
import WebSearchUI
import MapResourceToAvatarSizes
import SolidRoundedButtonNode
import AudioBlob
import DeviceAccess
let panelBackgroundColor = UIColor(rgb: 0x1c1c1e)
let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e)
let fullscreenBackgroundColor = UIColor(rgb: 0x000000)
private let smallButtonSize = CGSize(width: 36.0, height: 36.0)
private let sideButtonSize = CGSize(width: 56.0, height: 56.0)
private let topPanelHeight: CGFloat = 63.0
let bottomAreaHeight: CGFloat = 206.0
private let fullscreenBottomAreaHeight: CGFloat = 80.0
private let bottomGradientHeight: CGFloat = 70.0
func decorationCornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? {
if !top && !bottom {
return nil
}
return generateImage(CGSize(width: 50.0, height: 50.0), rotatedContext: { (size, context) in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor((dark ? fullscreenBackgroundColor : panelBackgroundColor).cgColor)
context.fill(bounds)
context.setBlendMode(.clear)
var corners: UIRectCorner = []
if top {
corners.insert(.topLeft)
corners.insert(.topRight)
}
if bottom {
corners.insert(.bottomLeft)
corners.insert(.bottomRight)
}
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: 11.0, height: 11.0))
context.addPath(path.cgPath)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25)
}
func decorationTopCornersImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 50.0, height: 110.0), rotatedContext: { (size, context) in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor((dark ? fullscreenBackgroundColor : panelBackgroundColor).cgColor)
context.fill(bounds)
context.setBlendMode(.clear)
var corners: UIRectCorner = []
corners.insert(.topLeft)
corners.insert(.topRight)
let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 60.0, width: 50.0, height: 50.0), byRoundingCorners: corners, cornerRadii: CGSize(width: 11.0, height: 11.0))
context.addPath(path.cgPath)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 32)
}
func decorationBottomCornersImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 50.0, height: 110.0), rotatedContext: { (size, context) in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor((dark ? fullscreenBackgroundColor : panelBackgroundColor).cgColor)
context.fill(bounds)
context.setBlendMode(.clear)
var corners: UIRectCorner = []
corners.insert(.bottomLeft)
corners.insert(.bottomRight)
let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: 50.0, height: 50.0), byRoundingCorners: corners, cornerRadii: CGSize(width: 11.0, height: 11.0))
context.addPath(path.cgPath)
context.fillPath()
})?.resizableImage(withCapInsets: UIEdgeInsets(top: 25.0, left: 25.0, bottom: 0.0, right: 25.0), resizingMode: .stretch)
}
private func decorationBottomGradientImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 24.0, height: bottomGradientHeight), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let color = dark ? fullscreenBackgroundColor : panelBackgroundColor
let colorsArray = [color.withAlphaComponent(0.0).cgColor, color.cgColor] as CFArray
var locations: [CGFloat] = [1.0, 0.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
struct VoiceChatPeerEntry: Identifiable {
enum State {
case listening
case speaking
case invited
case raisedHand
}
var peer: Peer
var about: String?
var isMyPeer: Bool
var videoEndpointId: String?
var videoPaused: Bool
var presentationEndpointId: String?
var presentationPaused: Bool
var effectiveSpeakerVideoEndpointId: String?
var state: State
var muteState: GroupCallParticipantsContext.Participant.MuteState?
var canManageCall: Bool
var volume: Int32?
var raisedHand: Bool
var displayRaisedHandStatus: Bool
var active: Bool
var isLandscape: Bool
var effectiveVideoEndpointId: String? {
return self.presentationEndpointId ?? self.videoEndpointId
}
init(
peer: Peer,
about: String?,
isMyPeer: Bool,
videoEndpointId: String?,
videoPaused: Bool,
presentationEndpointId: String?,
presentationPaused: Bool,
effectiveSpeakerVideoEndpointId: String?,
state: State,
muteState: GroupCallParticipantsContext.Participant.MuteState?,
canManageCall: Bool,
volume: Int32?,
raisedHand: Bool,
displayRaisedHandStatus: Bool,
active: Bool,
isLandscape: Bool
) {
self.peer = peer
self.about = about
self.isMyPeer = isMyPeer
self.videoEndpointId = videoEndpointId
self.videoPaused = videoPaused
self.presentationEndpointId = presentationEndpointId
self.presentationPaused = presentationPaused
self.effectiveSpeakerVideoEndpointId = effectiveSpeakerVideoEndpointId
self.state = state
self.muteState = muteState
self.canManageCall = canManageCall
self.volume = volume
self.raisedHand = raisedHand
self.displayRaisedHandStatus = displayRaisedHandStatus
self.active = active
self.isLandscape = isLandscape
}
var stableId: PeerId {
return self.peer.id
}
static func ==(lhs: VoiceChatPeerEntry, rhs: VoiceChatPeerEntry) -> Bool {
if !lhs.peer.isEqual(rhs.peer) {
return false
}
if lhs.about != rhs.about {
return false
}
if lhs.isMyPeer != rhs.isMyPeer {
return false
}
if lhs.videoEndpointId != rhs.videoEndpointId {
return false
}
if lhs.videoPaused != rhs.videoPaused {
return false
}
if lhs.presentationEndpointId != rhs.presentationEndpointId {
return false
}
if lhs.presentationPaused != rhs.presentationPaused {
return false
}
if lhs.effectiveSpeakerVideoEndpointId != rhs.effectiveSpeakerVideoEndpointId {
return false
}
if lhs.state != rhs.state {
return false
}
if lhs.muteState != rhs.muteState {
return false
}
if lhs.canManageCall != rhs.canManageCall {
return false
}
if lhs.volume != rhs.volume {
return false
}
if lhs.raisedHand != rhs.raisedHand {
return false
}
if lhs.displayRaisedHandStatus != rhs.displayRaisedHandStatus {
return false
}
if lhs.active != rhs.active {
return false
}
if lhs.isLandscape != rhs.isLandscape {
return false
}
return true
}
}
public final class VoiceChatController: ViewController {
enum DisplayMode {
case modal(isExpanded: Bool, isFilled: Bool)
case fullscreen(controlsHidden: Bool)
}
fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private struct ListTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isLoading: Bool
let isEmpty: Bool
let canInvite: Bool
let crossFade: Bool
let count: Int
let animated: Bool
}
private final class Interaction {
let updateIsMuted: (PeerId, Bool) -> Void
let switchToPeer: (PeerId, String?, Bool) -> Void
let openInvite: () -> Void
let peerContextAction: (VoiceChatPeerEntry, ASDisplayNode, ContextGesture?, Bool) -> Void
let getPeerVideo: (String, GroupVideoNode.Position) -> GroupVideoNode?
var isExpanded: Bool = false
private var audioLevels: [PeerId: ValuePipe<Float>] = [:]
var updateAvatarPromise = Promise<(TelegramMediaImageRepresentation, Float)?>(nil)
init(
updateIsMuted: @escaping (PeerId, Bool) -> Void,
switchToPeer: @escaping (PeerId, String?, Bool) -> Void,
openInvite: @escaping () -> Void,
peerContextAction: @escaping (VoiceChatPeerEntry, ASDisplayNode, ContextGesture?, Bool) -> Void,
getPeerVideo: @escaping (String, GroupVideoNode.Position) -> GroupVideoNode?
) {
self.updateIsMuted = updateIsMuted
self.switchToPeer = switchToPeer
self.openInvite = openInvite
self.peerContextAction = peerContextAction
self.getPeerVideo = getPeerVideo
}
func getAudioLevel(_ peerId: PeerId) -> Signal<Float, NoError> {
let signal: Signal<Float, NoError>
if let current = self.audioLevels[peerId] {
signal = current.signal()
} else {
let value = ValuePipe<Float>()
self.audioLevels[peerId] = value
signal = value.signal()
}
return signal
|> mapToSignal { value in
return .single(value)
}
}
func updateAudioLevels(_ levels: [(PeerId, UInt32, Float, Bool)], reset: Bool = false) {
var updated = Set<PeerId>()
for (peerId, _, level, _) in levels {
if let pipe = self.audioLevels[peerId] {
if reset {
pipe.putNext(level)
} else {
pipe.putNext(max(0.001, level))
}
updated.insert(peerId)
}
}
if !reset {
for (peerId, pipe) in self.audioLevels {
if !updated.contains(peerId) {
pipe.putNext(0.0)
}
}
}
}
}
private enum EntryId: Hashable {
case tiles
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 .tiles:
switch rhs {
case .tiles:
return true
default:
return false
}
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 tiles([VoiceChatTileItem], VoiceChatTileLayoutMode, Int32, Bool)
case invite(PresentationTheme, PresentationStrings, String, Bool)
case peer(VoiceChatPeerEntry, Int32)
var stableId: EntryId {
switch self {
case .tiles:
return .tiles
case .invite:
return .invite
case let .peer(peerEntry, _):
return .peerId(peerEntry.peer.id)
}
}
static func ==(lhs: ListEntry, rhs: ListEntry) -> Bool {
switch lhs {
case let .tiles(lhsTiles, lhsLayoutMode, lhsVideoLimit, lhsReachedLimit):
if case let .tiles(rhsTiles, rhsLayoutMode, rhsVideoLimit, rhsReachedLimit) = rhs, lhsTiles == rhsTiles, lhsLayoutMode == rhsLayoutMode, lhsVideoLimit == rhsVideoLimit, lhsReachedLimit == rhsReachedLimit {
return true
} else {
return false
}
case let .invite(lhsTheme, lhsStrings, lhsText, lhsIsLink):
if case let .invite(rhsTheme, rhsStrings, rhsText, rhsIsLink) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText, lhsIsLink == rhsIsLink {
return true
} else {
return false
}
case let .peer(lhsPeerEntry, lhsIndex):
switch rhs {
case let .peer(rhsPeerEntry, rhsIndex):
return lhsPeerEntry == rhsPeerEntry && lhsIndex == rhsIndex
default:
return false
}
}
}
static func <(lhs: ListEntry, rhs: ListEntry) -> Bool {
switch lhs {
case .tiles:
return true
case .invite:
return false
case let .peer(_, lhsIndex):
switch rhs {
case .tiles:
return false
case let .peer(_, rhsIndex):
return lhsIndex < rhsIndex
case .invite:
return true
}
}
}
func tileItem(context: AccountContext, presentationData: PresentationData, interaction: Interaction, isTablet: Bool, videoEndpointId: String, videoReady: Bool, videoTimeouted: Bool, videoIsPaused: Bool, showAsPresentation: Bool, secondary: Bool) -> VoiceChatTileItem? {
guard case let .peer(peerEntry, _) = self else {
return nil
}
let peer = peerEntry.peer
let icon: VoiceChatTileItem.Icon
var text: VoiceChatParticipantItem.ParticipantText
var additionalText: VoiceChatParticipantItem.ParticipantText?
var speaking = false
var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon()
let yourText: String
if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil {
yourText = presentationData.strings.VoiceChat_TapToAddPhotoOrBio
} else if peer.smallProfileImage == nil {
yourText = presentationData.strings.VoiceChat_TapToAddPhoto
} else if (peerEntry.about?.isEmpty ?? true) {
yourText = presentationData.strings.VoiceChat_TapToAddBio
} else {
yourText = presentationData.strings.VoiceChat_You
}
var state = peerEntry.state
if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute {
state = .listening
}
switch state {
case .listening:
if peerEntry.isMyPeer {
text = .text(yourText, textIcon, .accent)
} else if let muteState = peerEntry.muteState, muteState.mutedByYou {
text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive)
} else if let about = peerEntry.about, !about.isEmpty {
text = .text(about, textIcon, .generic)
} else {
text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic)
}
if let muteState = peerEntry.muteState, muteState.mutedByYou {
icon = .microphone(true)
additionalText = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive)
} else {
icon = .microphone(peerEntry.muteState != nil)
}
case .speaking:
if let muteState = peerEntry.muteState, muteState.mutedByYou {
text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive)
icon = .microphone(true)
additionalText = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive)
} else {
if peerEntry.volume != nil {
textIcon.insert(.volume)
}
let volumeValue = peerEntry.volume.flatMap { $0 / 100 }
if let volume = volumeValue, volume != 100 {
text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").string, textIcon, .constructive)
} else {
text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive)
}
icon = .microphone(false)
speaking = true
}
case .raisedHand, .invited:
text = .none
icon = .none
}
if let about = peerEntry.about, !about.isEmpty {
textIcon = []
text = .text(about, textIcon, .generic)
}
return VoiceChatTileItem(account: context.account, peer: peerEntry.peer, videoEndpointId: videoEndpointId, videoReady: videoReady, videoTimeouted: videoTimeouted, isVideoLimit: false, videoLimit: 0, isPaused: videoIsPaused, isOwnScreencast: peerEntry.presentationEndpointId == videoEndpointId && peerEntry.isMyPeer, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, speaking: speaking, secondary: secondary, isTablet: isTablet, icon: showAsPresentation ? .presentation : icon, text: text, additionalText: additionalText, action: {
interaction.switchToPeer(peer.id, videoEndpointId, !secondary)
}, contextAction: { node, gesture in
interaction.peerContextAction(peerEntry, node, gesture, false)
}, getVideo: { position in
return interaction.getPeerVideo(videoEndpointId, position)
}, getAudioLevel: {
return interaction.getAudioLevel(peerEntry.peer.id)
})
}
func fullscreenItem(context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListViewItem {
switch self {
case .tiles:
return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: "", icon: .none, action: {
})
case .invite:
return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: "", icon: .generic(UIImage(bundleImageName: "Chat/Context Menu/AddUser")!), action: {
interaction.openInvite()
})
case let .peer(peerEntry, _):
let peer = peerEntry.peer
var textColor: VoiceChatFullscreenParticipantItem.Color = .generic
var color: VoiceChatFullscreenParticipantItem.Color = .generic
let icon: VoiceChatFullscreenParticipantItem.Icon
var text: VoiceChatParticipantItem.ParticipantText
var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon()
let yourText: String
if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil {
yourText = presentationData.strings.VoiceChat_TapToAddPhotoOrBio
} else if peer.smallProfileImage == nil {
yourText = presentationData.strings.VoiceChat_TapToAddPhoto
} else if (peerEntry.about?.isEmpty ?? true) {
yourText = presentationData.strings.VoiceChat_TapToAddBio
} else {
yourText = presentationData.strings.VoiceChat_You
}
var state = peerEntry.state
if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute {
state = .listening
}
switch state {
case .listening:
if peerEntry.isMyPeer {
text = .text(yourText, textIcon, .accent)
} else if let muteState = peerEntry.muteState, muteState.mutedByYou {
text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive)
} else if let about = peerEntry.about, !about.isEmpty {
text = .text(about, textIcon, .generic)
} else {
text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic)
}
if let muteState = peerEntry.muteState, muteState.mutedByYou {
textColor = .destructive
color = .destructive
icon = .microphone(true, UIColor(rgb: 0xff3b30))
} else {
icon = .microphone(peerEntry.muteState != nil, UIColor.white)
color = .accent
}
case .speaking:
if let muteState = peerEntry.muteState, muteState.mutedByYou {
text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive)
textColor = .destructive
color = .destructive
icon = .microphone(true, UIColor(rgb: 0xff3b30))
} else {
if peerEntry.volume != nil {
textIcon.insert(.volume)
}
let volumeValue = peerEntry.volume.flatMap { $0 / 100 }
if let volume = volumeValue, volume != 100 {
text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").string, textIcon, .constructive)
} else {
text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive)
}
icon = .microphone(false, UIColor(rgb: 0x34c759))
textColor = .constructive
color = .constructive
}
case .raisedHand:
text = .none
textColor = .accent
icon = .wantsToSpeak
case .invited:
text = .none
icon = .none
}
if let about = peerEntry.about, !about.isEmpty {
textIcon = []
text = .text(about, textIcon, .generic)
}
var videoEndpointId = peerEntry.effectiveVideoEndpointId
var otherVideoEndpointId: String?
let hasBothVideos = peerEntry.presentationEndpointId != nil && peerEntry.videoEndpointId != nil
if hasBothVideos {
if let effectiveVideoEndpointId = peerEntry.effectiveSpeakerVideoEndpointId {
if effectiveVideoEndpointId == peerEntry.videoEndpointId {
videoEndpointId = peerEntry.presentationEndpointId
otherVideoEndpointId = videoEndpointId
} else if effectiveVideoEndpointId == peerEntry.presentationEndpointId {
videoEndpointId = peerEntry.videoEndpointId
otherVideoEndpointId = videoEndpointId
}
}
}
var isPaused = false
if videoEndpointId == peerEntry.videoEndpointId {
isPaused = peerEntry.videoPaused
} else if videoEndpointId == peerEntry.presentationEndpointId {
isPaused = peerEntry.presentationPaused
}
return VoiceChatFullscreenParticipantItem(presentationData: ItemListPresentationData(presentationData), nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peerEntry.peer, videoEndpointId: videoEndpointId, isPaused: isPaused, icon: icon, text: text, textColor: textColor, color: color, isLandscape: peerEntry.isLandscape, active: peerEntry.active, showVideoWhenActive: otherVideoEndpointId != nil, getAudioLevel: { return interaction.getAudioLevel(peerEntry.peer.id) }, getVideo: {
if let videoEndpointId = videoEndpointId {
return interaction.getPeerVideo(videoEndpointId, .list)
} else {
return nil
}
}, action: { _ in
interaction.switchToPeer(peerEntry.peer.id, otherVideoEndpointId, false)
}, contextAction: { node, gesture in
interaction.peerContextAction(peerEntry, node, gesture, true)
}, getUpdatingAvatar: {
return interaction.updateAvatarPromise.get()
})
}
}
func item(context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListViewItem {
switch self {
case let .tiles(tiles, layoutMode, videoLimit, reachedLimit):
return VoiceChatTilesGridItem(context: context, tiles: tiles, layoutMode: layoutMode, videoLimit: videoLimit, reachedLimit: reachedLimit, getIsExpanded: {
return interaction.isExpanded
})
case let .invite(_, _, text, isLink):
return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .generic(UIImage(bundleImageName: isLink ? "Chat/Context Menu/Link" : "Chat/Context Menu/AddUser")!), action: {
interaction.openInvite()
})
case let .peer(peerEntry, _):
let peer = peerEntry.peer
var text: VoiceChatParticipantItem.ParticipantText
var expandedText: VoiceChatParticipantItem.ParticipantText?
let icon: VoiceChatParticipantItem.Icon
var state = peerEntry.state
if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute {
state = .listening
}
var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon()
let yourText: String
if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil {
yourText = presentationData.strings.VoiceChat_TapToAddPhotoOrBio
} else if peer.smallProfileImage == nil {
yourText = presentationData.strings.VoiceChat_TapToAddPhoto
} else if (peerEntry.about?.isEmpty ?? true) {
yourText = presentationData.strings.VoiceChat_TapToAddBio
} else {
yourText = presentationData.strings.VoiceChat_You
}
switch state {
case .listening:
if peerEntry.isMyPeer {
text = .text(yourText, textIcon, .accent)
} else if let muteState = peerEntry.muteState, muteState.mutedByYou {
text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive)
} else if let about = peerEntry.about, !about.isEmpty {
text = .text(about, textIcon, .generic)
} else {
text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic)
}
let microphoneColor: UIColor
if let muteState = peerEntry.muteState, !muteState.canUnmute || muteState.mutedByYou {
microphoneColor = UIColor(rgb: 0xff3b30)
} else {
microphoneColor = UIColor(rgb: 0x979797)
}
icon = .microphone(peerEntry.muteState != nil, microphoneColor)
case .speaking:
if let muteState = peerEntry.muteState, muteState.mutedByYou {
text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive)
icon = .microphone(true, UIColor(rgb: 0xff3b30))
} else {
if peerEntry.volume != nil {
textIcon.insert(.volume)
}
let volumeValue = peerEntry.volume.flatMap { $0 / 100 }
if let volume = volumeValue, volume != 100 {
text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").string, textIcon, .constructive)
} else {
text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive)
}
icon = .microphone(false, UIColor(rgb: 0x34c759))
}
case .invited:
text = .text(presentationData.strings.VoiceChat_StatusInvited, textIcon, .generic)
icon = .invite(true)
case .raisedHand:
if peerEntry.isMyPeer && !peerEntry.displayRaisedHandStatus {
text = .text(yourText, textIcon, .accent)
} else if let about = peerEntry.about, !about.isEmpty && !peerEntry.displayRaisedHandStatus {
text = .text(about, textIcon, .generic)
} else {
text = .text(presentationData.strings.VoiceChat_StatusWantsToSpeak, textIcon, .accent)
}
icon = .wantsToSpeak
}
if let about = peerEntry.about, !about.isEmpty {
textIcon = []
expandedText = .text(about, textIcon, .generic)
}
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, text: text, expandedText: expandedText, icon: icon, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, action: { node in
if let node = node {
interaction.peerContextAction(peerEntry, node, nil, false)
}
}, contextAction: { node, gesture in
interaction.peerContextAction(peerEntry, node, gesture, false)
}, getIsExpanded: {
return interaction.isExpanded
}, getUpdatingAvatar: {
return interaction.updateAvatarPromise.get()
})
}
}
}
private func preparedTransition(from fromEntries: [ListEntry], to toEntries: [ListEntry], isLoading: Bool, isEmpty: Bool, canInvite: Bool, crossFade: Bool, animated: 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) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, canInvite: canInvite, crossFade: crossFade, count: toEntries.count, animated: animated)
}
private func preparedFullscreenTransition(from fromEntries: [ListEntry], to toEntries: [ListEntry], isLoading: Bool, isEmpty: Bool, canInvite: Bool, crossFade: Bool, animated: 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) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.fullscreenItem(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.fullscreenItem(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
return ListTransition(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, canInvite: canInvite, crossFade: crossFade, count: toEntries.count, animated: animated)
}
private let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil)
private var configuration: VoiceChatConfiguration?
private weak var controller: VoiceChatController?
private let sharedContext: SharedAccountContext
private let context: AccountContext
private let call: PresentationGroupCall
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var darkTheme: PresentationTheme
private let dimNode: ASDisplayNode
private let contentContainer: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let listContainer: ASDisplayNode
private let listNode: ListView
private let fullscreenListContainer: ASDisplayNode
private let fullscreenListNode: ListView
private let tileGridNode: VoiceChatTileGridNode
private let topPanelNode: ASDisplayNode
private let topPanelEdgeNode: ASDisplayNode
private let topPanelBackgroundNode: ASDisplayNode
private let optionsButton: VoiceChatHeaderButton
private let closeButton: VoiceChatHeaderButton
private let panelButton: VoiceChatHeaderButton
private let topCornersNode: ASImageNode
fileprivate let bottomPanelNode: ASDisplayNode
private let bottomGradientNode: ASDisplayNode
private let bottomPanelBackgroundNode: ASDisplayNode
private let bottomCornersNode: ASImageNode
fileprivate let audioButton: CallControllerButtonItemNode
fileprivate let cameraButton: CallControllerButtonItemNode
fileprivate let switchCameraButton: CallControllerButtonItemNode
fileprivate let leaveButton: CallControllerButtonItemNode
fileprivate let actionButton: VoiceChatActionButton
private let leftBorderNode: ASDisplayNode
private let rightBorderNode: ASDisplayNode
private let mainStageContainerNode: ASDisplayNode
private let mainStageBackgroundNode: ASDisplayNode
private let mainStageNode: VoiceChatMainStageNode
private let transitionMaskView: UIView
private let transitionMaskTopFillLayer: CALayer
private let transitionMaskFillLayer: CALayer
private let transitionMaskGradientLayer: CAGradientLayer
private let transitionMaskBottomFillLayer: CALayer
private let transitionContainerNode: ASDisplayNode
private var isScheduling = false
private let timerNode: VoiceChatTimerNode
private var pickerView: UIDatePicker?
private let dateFormatter: DateFormatter
private let scheduleTextNode: ImmediateTextNode
private let scheduleCancelButton: SolidRoundedButtonNode
private var scheduleButtonTitle = ""
private let titleNode: VoiceChatTitleNode
private var enqueuedTransitions: [ListTransition] = []
private var enqueuedFullscreenTransitions: [ListTransition] = []
private var validLayout: (ContainerViewLayout, CGFloat)?
private var didSetContentsReady: Bool = false
private var didSetDataReady: Bool = false
private var isFirstTime = true
private var topInset: CGFloat?
private var animatingInsertion = false
private var animatingExpansion = false
private var animatingAppearance = false
private var animatingButtonsSwap = false
private var animatingMainStage = false
private var animatingContextMenu = false
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat)?
private var isPanning = false
private var peer: Peer?
private var currentTitle: String = ""
private var currentTitleIsCustom = false
private var currentSubtitle: String = ""
private var currentSpeakingSubtitle: String?
private var currentCallMembers: ([GroupCallParticipantsContext.Participant], String?)?
private var currentInvitedPeers: [Peer]?
private var currentSpeakingPeers: Set<PeerId>?
private var currentContentOffset: CGFloat?
private var currentNormalButtonColor: UIColor?
private var currentActiveButtonColor: UIColor?
private var myEntry: VoiceChatPeerEntry?
private var mainEntry: VoiceChatPeerEntry?
private var currentEntries: [ListEntry] = []
private var currentFullscreenEntries: [ListEntry] = []
private var currentTileItems: [VoiceChatTileItem] = []
private var displayPanelVideos = false
private var joinedVideo: Bool?
private var peerViewDisposable: Disposable?
private let leaveDisposable = MetaDisposable()
private var isMutedDisposable: Disposable?
private var isNoiseSuppressionEnabled: Bool = true
private var isNoiseSuppressionEnabledDisposable: Disposable?
private var callStateDisposable: Disposable?
private var pushingToTalk = false
private var temporaryPushingToTalk = false
private let hapticFeedback = HapticFeedback()
private var callState: PresentationGroupCallState?
private var currentLoadToken: String?
private var effectiveMuteState: GroupCallParticipantsContext.Participant.MuteState? {
if self.pushingToTalk {
return nil
} else {
return self.callState?.muteState
}
}
private var audioOutputStateDisposable: Disposable?
private var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)?
private var audioLevelsDisposable: Disposable?
private var myAudioLevelDisposable: Disposable?
private var isSpeakingDisposable: Disposable?
private var memberStatesDisposable: Disposable?
private var actionButtonColorDisposable: Disposable?
private var itemInteraction: Interaction?
private let inviteDisposable = MetaDisposable()
private let memberEventsDisposable = MetaDisposable()
private let reconnectedAsEventsDisposable = MetaDisposable()
private let stateVersionDisposable = MetaDisposable()
private var applicationStateDisposable: Disposable?
private let displayAsPeersPromise = Promise<[FoundPeer]>([])
private let inviteLinksPromise = Promise<GroupCallInviteLinks?>(nil)
private var raisedHandDisplayDisposables: [PeerId: Disposable] = [:]
private var displayedRaisedHands = Set<PeerId>() {
didSet {
self.displayedRaisedHandsPromise.set(self.displayedRaisedHands)
}
}
private let displayedRaisedHandsPromise = ValuePromise<Set<PeerId>>(Set())
private var requestedVideoSources = Set<String>()
private var requestedVideoChannels: [PresentationGroupCallRequestedVideo] = []
private var videoRenderingContext: VideoRenderingContext
private var videoNodes: [String: GroupVideoNode] = [:]
private var wideVideoNodes = Set<String>()
private var videoOrder: [String] = []
private var readyVideoEndpointIds = Set<String>()
private var readyVideoEndpointIdsPromise = ValuePromise<Set<String>>(Set())
private var timeoutedEndpointIds = Set<String>()
private var readyVideoDisposables = DisposableDict<String>()
private var myPeerVideoReadyDisposable = MetaDisposable()
private var peerIdToEndpointId: [PeerId: String] = [:]
private var currentSpeakers: [PeerId] = []
private var currentDominantSpeaker: (PeerId, String?, Double)?
private var currentForcedSpeaker: (PeerId, String?)?
private var effectiveSpeaker: (PeerId, String?, Bool, Bool, Bool)?
private var updateAvatarDisposable = MetaDisposable()
private let updateAvatarPromise = Promise<(TelegramMediaImageRepresentation, Float)?>(nil)
private var currentUpdatingAvatar: TelegramMediaImageRepresentation?
private var connectedOnce = false
private var ignoreConnecting = false
private var ignoreConnectingTimer: SwiftSignalKit.Timer?
private var displayUnmuteTooltipTimer: SwiftSignalKit.Timer?
private var dismissUnmuteTooltipTimer: SwiftSignalKit.Timer?
private var lastUnmuteTooltipDisplayTimestamp: Double?
private var panelHidden = false
private var displayMode: DisplayMode = .modal(isExpanded: false, isFilled: false) {
didSet {
if case let .modal(isExpanded, _) = self.displayMode {
self.itemInteraction?.isExpanded = isExpanded
} else {
self.itemInteraction?.isExpanded = true
}
}
}
private var isExpanded: Bool {
switch self.displayMode {
case .modal(true, _), .fullscreen:
return true
default:
return false
}
}
private var statsDisposable: Disposable?
init(controller: VoiceChatController, sharedContext: SharedAccountContext, call: PresentationGroupCall) {
self.controller = controller
self.sharedContext = sharedContext
self.context = call.accountContext
self.call = call
self.videoRenderingContext = VideoRenderingContext()
self.isScheduling = call.schedulePending
let presentationData = sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.darkTheme = defaultDarkColorPresentationTheme
self.currentSubtitle = self.presentationData.strings.SocksProxySetup_ProxyStatusConnecting
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.contentContainer = ASDisplayNode()
self.contentContainer.isHidden = true
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = self.isScheduling ? panelBackgroundColor : secondaryPanelBackgroundColor
self.backgroundNode.clipsToBounds = false
self.listContainer = ASDisplayNode()
self.listNode = ListView()
self.listNode.alpha = self.isScheduling ? 0.0 : 1.0
self.listNode.isUserInteractionEnabled = !self.isScheduling
self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3)
self.listNode.clipsToBounds = true
self.listNode.scroller.bounces = false
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.fullscreenListContainer = ASDisplayNode()
self.fullscreenListContainer.isHidden = true
self.fullscreenListNode = ListView()
self.fullscreenListNode.transform = CATransform3DMakeRotation(-CGFloat(CGFloat.pi / 2.0), 0.0, 0.0, 1.0)
self.fullscreenListNode.clipsToBounds = true
self.fullscreenListNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.tileGridNode = VoiceChatTileGridNode(context: self.context)
self.topPanelNode = ASDisplayNode()
self.topPanelNode.clipsToBounds = false
self.topPanelBackgroundNode = ASDisplayNode()
self.topPanelBackgroundNode.backgroundColor = panelBackgroundColor
self.topPanelBackgroundNode.isUserInteractionEnabled = false
self.topPanelEdgeNode = ASDisplayNode()
self.topPanelEdgeNode.backgroundColor = panelBackgroundColor
self.topPanelEdgeNode.cornerRadius = 12.0
self.topPanelEdgeNode.isUserInteractionEnabled = false
if #available(iOS 11.0, *) {
self.topPanelEdgeNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
}
self.optionsButton = VoiceChatHeaderButton(context: self.context)
self.optionsButton.setContent(.more(optionsCircleImage(dark: false)))
self.closeButton = VoiceChatHeaderButton(context: self.context)
self.closeButton.setContent(.image(closeButtonImage(dark: false)))
self.panelButton = VoiceChatHeaderButton(context: self.context, wide: true)
self.panelButton.setContent(.image(panelButtonImage(dark: false)))
self.titleNode = VoiceChatTitleNode(theme: self.presentationData.theme)
self.topCornersNode = ASImageNode()
self.topCornersNode.displaysAsynchronously = false
self.topCornersNode.displayWithoutProcessing = true
self.topCornersNode.image = decorationTopCornersImage(dark: false)
self.topCornersNode.isUserInteractionEnabled = false
self.bottomPanelNode = ASDisplayNode()
self.bottomPanelNode.clipsToBounds = false
self.bottomPanelBackgroundNode = ASDisplayNode()
self.bottomPanelBackgroundNode.backgroundColor = panelBackgroundColor
self.bottomPanelBackgroundNode.isUserInteractionEnabled = false
self.bottomGradientNode = ASDisplayNode()
self.bottomGradientNode.displaysAsynchronously = false
self.bottomGradientNode.backgroundColor = decorationBottomGradientImage(dark: false).flatMap { UIColor(patternImage: $0) }
self.bottomCornersNode = ASImageNode()
self.bottomCornersNode.displaysAsynchronously = false
self.bottomCornersNode.displayWithoutProcessing = true
self.bottomCornersNode.image = decorationBottomCornersImage(dark: false)
self.bottomCornersNode.isUserInteractionEnabled = false
self.audioButton = CallControllerButtonItemNode()
self.cameraButton = CallControllerButtonItemNode(largeButtonSize: sideButtonSize.width)
self.switchCameraButton = CallControllerButtonItemNode()
self.switchCameraButton.alpha = 0.0
self.switchCameraButton.isUserInteractionEnabled = false
self.leaveButton = CallControllerButtonItemNode()
self.actionButton = VoiceChatActionButton()
if self.isScheduling {
self.cameraButton.alpha = 0.0
self.cameraButton.isUserInteractionEnabled = false
self.audioButton.alpha = 0.0
self.audioButton.isUserInteractionEnabled = false
self.leaveButton.alpha = 0.0
self.leaveButton.isUserInteractionEnabled = false
}
self.leftBorderNode = ASDisplayNode()
self.leftBorderNode.backgroundColor = panelBackgroundColor
self.leftBorderNode.isUserInteractionEnabled = false
self.leftBorderNode.clipsToBounds = false
self.rightBorderNode = ASDisplayNode()
self.rightBorderNode.backgroundColor = panelBackgroundColor
self.rightBorderNode.isUserInteractionEnabled = false
self.rightBorderNode.clipsToBounds = false
self.mainStageContainerNode = ASDisplayNode()
self.mainStageContainerNode.clipsToBounds = true
self.mainStageContainerNode.isUserInteractionEnabled = false
self.mainStageContainerNode.isHidden = true
self.mainStageBackgroundNode = ASDisplayNode()
self.mainStageBackgroundNode.backgroundColor = .black
self.mainStageBackgroundNode.alpha = 0.0
self.mainStageBackgroundNode.isUserInteractionEnabled = false
self.mainStageNode = VoiceChatMainStageNode(context: self.context, call: self.call)
self.transitionMaskView = UIView()
self.transitionMaskTopFillLayer = CALayer()
self.transitionMaskTopFillLayer.backgroundColor = UIColor.white.cgColor
self.transitionMaskTopFillLayer.opacity = 0.0
self.transitionMaskFillLayer = CALayer()
self.transitionMaskFillLayer.backgroundColor = UIColor.white.cgColor
self.transitionMaskGradientLayer = CAGradientLayer()
self.transitionMaskGradientLayer.colors = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
self.transitionMaskGradientLayer.locations = [0.0, 1.0]
self.transitionMaskGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
self.transitionMaskGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
self.transitionMaskBottomFillLayer = CALayer()
self.transitionMaskBottomFillLayer.backgroundColor = UIColor.white.cgColor
self.transitionMaskBottomFillLayer.opacity = 0.0
self.transitionMaskView.layer.addSublayer(self.transitionMaskTopFillLayer)
self.transitionMaskView.layer.addSublayer(self.transitionMaskFillLayer)
self.transitionMaskView.layer.addSublayer(self.transitionMaskGradientLayer)
self.transitionMaskView.layer.addSublayer(self.transitionMaskBottomFillLayer)
self.transitionContainerNode = ASDisplayNode()
self.transitionContainerNode.clipsToBounds = true
self.transitionContainerNode.isUserInteractionEnabled = false
self.transitionContainerNode.view.mask = self.transitionMaskView
// self.transitionContainerNode.view.addSubview(self.transitionMaskView)
self.scheduleTextNode = ImmediateTextNode()
self.scheduleTextNode.isHidden = !self.isScheduling
self.scheduleTextNode.isUserInteractionEnabled = false
self.scheduleTextNode.textAlignment = .center
self.scheduleTextNode.maximumNumberOfLines = 4
self.scheduleCancelButton = SolidRoundedButtonNode(title: self.presentationData.strings.Common_Cancel, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x2b2b2f), foregroundColor: .white), height: 52.0, cornerRadius: 10.0)
self.scheduleCancelButton.isHidden = !self.isScheduling
self.dateFormatter = DateFormatter()
self.dateFormatter.timeStyle = .none
self.dateFormatter.dateStyle = .short
self.dateFormatter.timeZone = TimeZone.current
self.timerNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)
self.timerNode.isHidden = true
super.init()
let context = self.context
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId)
|> map { peer in
return [FoundPeer(peer: peer, subscribers: nil)]
}
self.isNoiseSuppressionEnabledDisposable = (call.isNoiseSuppressionEnabled
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.isNoiseSuppressionEnabled = value
})
let displayAsPeers: Signal<[FoundPeer], NoError> = currentAccountPeer
|> then(
combineLatest(currentAccountPeer, context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: call.peerId))
|> map { currentAccountPeer, availablePeers -> [FoundPeer] in
var result = currentAccountPeer
result.append(contentsOf: availablePeers)
return result
}
)
self.displayAsPeersPromise.set(displayAsPeers)
self.inviteLinksPromise.set(.single(nil)
|> then(call.inviteLinks))
self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in
let _ = self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
}, switchToPeer: { [weak self] peerId, videoEndpointId, expand in
if let strongSelf = self, strongSelf.connectedOnce {
if expand, let videoEndpointId = videoEndpointId {
strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime() + 3.0)
strongSelf.updateDisplayMode(.fullscreen(controlsHidden: false))
} else {
strongSelf.currentForcedSpeaker = nil
if peerId != strongSelf.currentDominantSpeaker?.0 || (videoEndpointId != nil && videoEndpointId != strongSelf.currentDominantSpeaker?.1) {
strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime())
}
strongSelf.updateMainVideo(waitForFullSize: true, updateMembers: true, force: true)
}
}
}, openInvite: { [weak self] in
guard let strongSelf = self else {
return
}
let groupPeerId = strongSelf.call.peerId
let groupPeer = strongSelf.context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(groupPeerId)
}
let _ = combineLatest(queue: Queue.mainQueue(), groupPeer, strongSelf.inviteLinksPromise.get() |> take(1)).start(next: { groupPeer, inviteLinks in
guard let strongSelf = self else {
return
}
guard let groupPeer = groupPeer else {
return
}
if let groupPeer = groupPeer as? TelegramChannel {
var canInviteMembers = true
if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) {
canInviteMembers = false
}
if !canInviteMembers {
if let inviteLinks = inviteLinks {
strongSelf.presentShare(inviteLinks)
}
return
}
}
var filters: [ChannelMembersSearchFilter] = []
if let (currentCallMembers, _) = strongSelf.currentCallMembers {
filters.append(.disable(Array(currentCallMembers.map { $0.peer.id })))
}
if let groupPeer = groupPeer as? TelegramChannel {
if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil {
filters.append(.excludeNonMembers)
}
} else if let groupPeer = groupPeer as? TelegramGroup {
if groupPeer.hasBannedPermission(.banAddMembers) {
filters.append(.excludeNonMembers)
}
}
filters.append(.excludeBots)
var dismissController: (() -> Void)?
let controller = ChannelMembersSearchController(context: strongSelf.context, peerId: groupPeer.id, forceTheme: strongSelf.darkTheme, mode: .inviteToCall, filters: filters, openPeer: { peer, participant in
guard let strongSelf = self else {
dismissController?()
return
}
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
if peer.id == strongSelf.callState?.myPeerId {
return
}
if let participant = participant {
dismissController?()
if strongSelf.call.invitePeer(participant.peer.id) {
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: participant.peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return false })
}
} else {
if let groupPeer = groupPeer as? TelegramChannel, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) {
let text = strongSelf.presentationData.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in
dismissController?()
if let strongSelf = self {
let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)])
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self {
strongSelf.presentUndoOverlay(content: .forward(savedMessages: false, text: strongSelf.presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return true })
}
})
}
})]), in: .window(.root))
} else {
let text: String
if let groupPeer = groupPeer as? TelegramChannel, case .broadcast = groupPeer.info {
text = strongSelf.presentationData.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
} else {
text = strongSelf.presentationData.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
}
strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: {
guard let strongSelf = self else {
return
}
if let groupPeer = groupPeer as? TelegramChannel {
let selfController = strongSelf.controller
let inviteDisposable = strongSelf.inviteDisposable
var inviteSignal = strongSelf.context.peerChannelMemberCategoriesContextsManager.addMembers(engine: strongSelf.context.engine, peerId: groupPeer.id, memberIds: [peer.id])
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
selfController?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
inviteSignal = inviteSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
inviteDisposable.set(nil)
}
inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { error in
dismissController?()
guard let strongSelf = self else {
return
}
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let text: String
switch error {
case .limitExceeded:
text = presentationData.strings.Channel_ErrorAddTooMuch
case .tooMuchJoined:
text = presentationData.strings.Invite_ChannelsTooMuch
case .generic:
text = presentationData.strings.Login_UnknownError
case .restricted:
text = presentationData.strings.Channel_ErrorAddBlocked
case .notMutualContact:
if case .broadcast = groupPeer.info {
text = presentationData.strings.Channel_AddUserLeftError
} else {
text = presentationData.strings.GroupInfo_AddUserLeftError
}
case .botDoesntSupportGroups:
text = presentationData.strings.Channel_BotDoesntSupportGroups
case .tooMuchBots:
text = presentationData.strings.Channel_TooMuchBots
case .bot:
text = presentationData.strings.Login_UnknownError
}
strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, completed: {
guard let strongSelf = self else {
dismissController?()
return
}
dismissController?()
if strongSelf.call.invitePeer(peer.id) {
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return false })
}
}))
} else if let groupPeer = groupPeer as? TelegramGroup {
let selfController = strongSelf.controller
let inviteDisposable = strongSelf.inviteDisposable
var inviteSignal = strongSelf.context.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id)
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
selfController?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
inviteSignal = inviteSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
inviteDisposable.set(nil)
}
inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { error in
dismissController?()
guard let strongSelf = self else {
return
}
let context = strongSelf.context
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
switch error {
case .privacy:
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peer.id)
|> deliverOnMainQueue).start(next: { peer in
self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(peer.compactDisplayTitle, peer.compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
})
case .notMutualContact:
strongSelf.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
case .tooManyChannels:
strongSelf.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
case .groupFull, .generic:
strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}, completed: {
guard let strongSelf = self else {
dismissController?()
return
}
dismissController?()
if strongSelf.call.invitePeer(peer.id) {
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return false })
}
}))
}
})]), in: .window(.root))
}
}
})
controller.copyInviteLink = {
dismissController?()
guard let strongSelf = self else {
return
}
let callPeerId = strongSelf.call.peerId
let _ = (strongSelf.context.account.postbox.transaction { transaction -> String? in
if let link = inviteLinks?.listenerLink {
return link
} else if let peer = transaction.getPeer(callPeerId), let addressName = peer.addressName, !addressName.isEmpty {
return "https://t.me/\(addressName)"
} else if let cachedData = transaction.getPeerCachedData(peerId: callPeerId) {
if let cachedData = cachedData as? CachedChannelData {
return cachedData.exportedInvitation?.link
} else if let cachedData = cachedData as? CachedGroupData {
return cachedData.exportedInvitation?.link
}
}
return nil
}
|> deliverOnMainQueue).start(next: { link in
guard let strongSelf = self else {
return
}
if let link = link {
UIPasteboard.general.string = link
strongSelf.presentUndoOverlay(content: .linkCopied(text: strongSelf.presentationData.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false })
}
})
}
dismissController = { [weak controller] in
controller?.dismiss()
}
strongSelf.controller?.push(controller)
})
}, peerContextAction: { [weak self] entry, sourceNode, gesture, fullscreen in
guard let strongSelf = self, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else {
return
}
let muteStatePromise = Promise<GroupCallParticipantsContext.Participant.MuteState?>(entry.muteState)
let itemsForEntry: (VoiceChatPeerEntry, GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { entry, muteState in
var items: [ContextMenuItem] = []
var hasVolumeSlider = false
let peer = entry.peer
if let muteState = muteState, !muteState.canUnmute || muteState.mutedByYou {
} else {
if entry.canManageCall || !entry.isMyPeer {
hasVolumeSlider = true
let minValue: CGFloat
if let callState = strongSelf.callState, callState.canManageCall && callState.adminIds.contains(peer.id) && muteState != nil {
minValue = 0.01
} else {
minValue = 0.0
}
items.append(.custom(VoiceChatVolumeContextItem(minValue: minValue, value: entry.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { newValue, finished in
if finished && newValue.isZero {
let updatedMuteState = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true)
muteStatePromise.set(.single(updatedMuteState))
} else {
strongSelf.call.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished)
}
}), true))
}
}
if entry.isMyPeer && !hasVolumeSlider && ((entry.about?.isEmpty ?? true) || entry.peer.smallProfileImage == nil) {
items.append(.custom(VoiceChatInfoContextItem(text: strongSelf.presentationData.strings.VoiceChat_ImproveYourProfileText, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: theme.actionSheet.primaryTextColor)
}), true))
}
if peer.id == strongSelf.callState?.myPeerId {
if entry.raisedHand {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_CancelSpeakRequest, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/RevokeSpeak"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
let _ = strongSelf.call.lowerHand()
f(.default)
})))
}
items.append(.action(ContextMenuActionItem(text: peer.smallProfileImage == nil ? strongSelf.presentationData.strings.VoiceChat_AddPhoto : strongSelf.presentationData.strings.VoiceChat_ChangePhoto, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
f(.default)
Queue.mainQueue().after(0.1) {
strongSelf.openAvatarForEditing(fromGallery: false, completion: {})
}
})))
items.append(.action(ContextMenuActionItem(text: (entry.about?.isEmpty ?? true) ? strongSelf.presentationData.strings.VoiceChat_AddBio : strongSelf.presentationData.strings.VoiceChat_EditBio, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
f(.default)
Queue.mainQueue().after(0.1) {
let maxBioLength: Int
if peer.id.namespace == Namespaces.Peer.CloudUser {
maxBioLength = 70
} else {
maxBioLength = 100
}
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditBioTitle, text: presentationData.strings.VoiceChat_EditBioText, placeholder: presentationData.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: presentationData.strings.VoiceChat_EditBioSave, value: entry.about, maxLength: maxBioLength, apply: { bio in
if let strongSelf = self, let bio = bio {
if peer.id.namespace == Namespaces.Peer.CloudUser {
let _ = (strongSelf.context.engine.accountData.updateAbout(about: bio)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}).start()
} else {
let _ = (strongSelf.context.engine.peers.updatePeerDescription(peerId: peer.id, description: bio)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}).start()
}
strongSelf.presentUndoOverlay(content: .info(text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess), action: { _ in return false })
}
})
self?.controller?.present(controller, in: .window(.root))
}
})))
if let peer = peer as? TelegramUser {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_ChangeName, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ChangeName"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
f(.default)
Queue.mainQueue().after(0.1) {
let controller = voiceChatUserNameController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: presentationData.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: presentationData.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: presentationData.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { firstAndLastName in
if let strongSelf = self, let (firstName, lastName) = firstAndLastName {
let _ = context.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).start()
strongSelf.presentUndoOverlay(content: .info(text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess), action: { _ in return false })
}
})
self?.controller?.present(controller, in: .window(.root))
}
})))
}
} else {
if let callState = strongSelf.callState, (callState.canManageCall || callState.adminIds.contains(strongSelf.context.account.peerId)) {
if callState.adminIds.contains(peer.id) {
if let _ = muteState {
} else {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MutePeer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
let _ = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true)
f(.default)
})))
}
} else {
if let muteState = muteState, !muteState.canUnmute {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_UnmutePeer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: entry.raisedHand ? "Call/Context Menu/AllowToSpeak" : "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
let _ = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: false)
f(.default)
strongSelf.presentUndoOverlay(content: .voiceChatCanSpeak(text: presentationData.strings.VoiceChat_UserCanNowSpeak(entry.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return true })
})))
} else {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MutePeer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
let _ = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true)
f(.default)
})))
}
}
} else {
if let muteState = muteState, muteState.mutedByYou {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_UnmuteForMe, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
let _ = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: false)
f(.default)
})))
} else {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MuteForMe, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self else {
return
}
let _ = strongSelf.call.updateMuteState(peerId: peer.id, isMuted: true)
f(.default)
})))
}
}
let openTitle: String
let openIcon: UIImage?
if [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peer.id.namespace) {
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
openTitle = strongSelf.presentationData.strings.VoiceChat_OpenChannel
openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels")
} else {
openTitle = strongSelf.presentationData.strings.VoiceChat_OpenGroup
openIcon = UIImage(bundleImageName: "Chat/Context Menu/Groups")
}
} else {
openTitle = strongSelf.presentationData.strings.Conversation_ContextMenuSendMessage
openIcon = UIImage(bundleImageName: "Chat/Context Menu/Message")
}
items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in
return generateTintedImage(image: openIcon, color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
guard let strongSelf = self, let navigationController = strongSelf.controller?.parentNavigationController else {
return
}
let context = strongSelf.context
strongSelf.controller?.dismiss(completion: {
Queue.mainQueue().after(0.3) {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer.id), keepStack: .always, purposefulAction: {}, peekData: nil))
}
})
f(.dismissWithoutContent)
})))
if let callState = strongSelf.callState, (callState.canManageCall && !callState.adminIds.contains(peer.id)), peer.id != strongSelf.call.peerId {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak self] c, _ in
c.dismiss(completion: {
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.call.peerId)
|> deliverOnMainQueue).start(next: { [weak self] chatPeer in
guard let strongSelf = self else {
return
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme))
var items: [ActionSheetItem] = []
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: EnginePeer(peer), chatPeer: EnginePeer(chatPeer), action: .removeFromGroup, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let _ = strongSelf.context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: strongSelf.context.engine, peerId: strongSelf.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start()
strongSelf.call.removedPeer(peer.id)
strongSelf.presentUndoOverlay(content: .banned(text: strongSelf.presentationData.strings.VoiceChat_RemovedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return false })
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
})
})
})))
}
}
return items
}
let items = muteStatePromise.get()
|> map { muteState -> [ContextMenuItem] in
return itemsForEntry(entry, muteState)
}
var centerVertically = entry.peer.smallProfileImage != nil || (!fullscreen && entry.effectiveVideoEndpointId != nil)
if let (layout, _) = strongSelf.validLayout, case .regular = layout.metrics.widthClass {
centerVertically = false
}
var useMaskView = true
if case .fullscreen = strongSelf.displayMode {
useMaskView = false
}
let dismissPromise = ValuePromise<Bool>(false)
let source = VoiceChatContextExtractedContentSource(sourceNode: sourceNode, maskView: useMaskView ? strongSelf.transitionMaskView : nil, keepInPlace: false, blurBackground: true, centerVertically: centerVertically, shouldBeDismissed: dismissPromise.get(), animateTransitionIn: { [weak self] in
if let strongSelf = self {
strongSelf.animatingContextMenu = true
strongSelf.updateDecorationsLayout(transition: .immediate)
if strongSelf.isLandscape {
strongSelf.transitionMaskTopFillLayer.opacity = 1.0
}
strongSelf.transitionContainerNode.view.mask = nil
strongSelf.transitionMaskBottomFillLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, removeOnCompletion: false, completion: { [weak self] _ in
Queue.mainQueue().after(0.3) {
self?.transitionMaskTopFillLayer.opacity = 0.0
self?.transitionMaskBottomFillLayer.removeAllAnimations()
self?.animatingContextMenu = false
self?.updateDecorationsLayout(transition: .immediate)
}
})
}
}, animateTransitionOut: { [weak self] in
if let strongSelf = self {
strongSelf.animatingContextMenu = true
strongSelf.updateDecorationsLayout(transition: .immediate)
strongSelf.transitionMaskTopFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4)
strongSelf.transitionMaskBottomFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, completion: { [weak self] _ in
self?.animatingContextMenu = false
self?.updateDecorationsLayout(transition: .immediate)
self?.transitionContainerNode.view.mask = self?.transitionMaskView
})
}
})
sourceNode.requestDismiss = {
dismissPromise.set(true)
}
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items, reactionItems: [], gesture: gesture)
contextController.useComplexItemsTransitionAnimation = true
strongSelf.controller?.presentInGlobalOverlay(contextController)
}, getPeerVideo: { [weak self] endpointId, position in
guard let strongSelf = self else {
return nil
}
var ignore = false
if case .mainstage = position {
ignore = false
} else if case .fullscreen = strongSelf.displayMode, !strongSelf.isPanning {
ignore = ![.mainstage, .list].contains(position)
} else {
ignore = position != .tile
}
if ignore {
return nil
}
if !strongSelf.readyVideoEndpointIds.contains(endpointId) {
return nil
}
for (listEndpointId, videoNode) in strongSelf.videoNodes {
if listEndpointId == endpointId {
if position != .mainstage && videoNode.isMainstageExclusive {
return nil
}
return videoNode
}
}
return nil
})
self.itemInteraction?.updateAvatarPromise = self.updateAvatarPromise
self.topPanelNode.addSubnode(self.topPanelEdgeNode)
self.topPanelNode.addSubnode(self.topPanelBackgroundNode)
self.topPanelNode.addSubnode(self.titleNode)
self.topPanelNode.addSubnode(self.optionsButton)
self.topPanelNode.addSubnode(self.closeButton)
self.topPanelNode.addSubnode(self.panelButton)
self.bottomPanelNode.addSubnode(self.cameraButton)
self.bottomPanelNode.addSubnode(self.audioButton)
self.bottomPanelNode.addSubnode(self.switchCameraButton)
self.bottomPanelNode.addSubnode(self.leaveButton)
self.bottomPanelNode.addSubnode(self.actionButton)
self.bottomPanelNode.addSubnode(self.scheduleCancelButton)
self.addSubnode(self.dimNode)
self.addSubnode(self.contentContainer)
self.contentContainer.addSubnode(self.backgroundNode)
self.contentContainer.addSubnode(self.listContainer)
self.contentContainer.addSubnode(self.topPanelNode)
self.listContainer.addSubnode(self.listNode)
self.listContainer.addSubnode(self.leftBorderNode)
self.listContainer.addSubnode(self.rightBorderNode)
self.listContainer.addSubnode(self.bottomCornersNode)
self.listContainer.addSubnode(self.topCornersNode)
self.contentContainer.addSubnode(self.bottomGradientNode)
self.contentContainer.addSubnode(self.bottomPanelBackgroundNode)
self.contentContainer.addSubnode(self.tileGridNode)
self.contentContainer.addSubnode(self.mainStageContainerNode)
self.contentContainer.addSubnode(self.transitionContainerNode)
self.contentContainer.addSubnode(self.bottomPanelNode)
self.contentContainer.addSubnode(self.timerNode)
self.contentContainer.addSubnode(self.scheduleTextNode)
self.contentContainer.addSubnode(self.fullscreenListContainer)
self.fullscreenListContainer.addSubnode(self.fullscreenListNode)
self.mainStageContainerNode.addSubnode(self.mainStageBackgroundNode)
self.mainStageContainerNode.addSubnode(self.mainStageNode)
self.updateDecorationsColors()
let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers
|> mapToSignal { ids -> Signal<[Peer], NoError> in
return context.account.postbox.transaction { transaction -> [Peer] in
return ids.compactMap(transaction.getPeer)
}
}
self.presentationDataDisposable = (sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.presentationData = presentationData
let sourceColor = presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor
let color: UIColor
if sourceColor.alpha < 1.0 {
color = presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor.mixedWith(sourceColor.withAlphaComponent(1.0), alpha: sourceColor.alpha)
} else {
color = sourceColor
}
strongSelf.actionButton.connectingColor = color
}
})
self.memberStatesDisposable = (combineLatest(queue: .mainQueue(),
self.call.state,
self.call.members,
invitedPeers,
self.displayAsPeersPromise.get(),
self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
)
|> mapToThrottled { values in
return .single(values)
|> then(.complete() |> delay(0.1, queue: Queue.mainQueue()))
}).start(next: { [weak self] state, callMembers, invitedPeers, displayAsPeers, preferencesView in
guard let strongSelf = self else {
return
}
let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
let configuration = VoiceChatConfiguration.with(appConfiguration: appConfiguration)
strongSelf.configuration = configuration
var animate = false
if strongSelf.callState != state {
if let previousCallState = strongSelf.callState {
var networkStateUpdated = false
if case .connecting = previousCallState.networkState, case .connected = state.networkState {
networkStateUpdated = true
strongSelf.connectedOnce = true
}
var canUnmuteUpdated = false
if previousCallState.muteState?.canUnmute != state.muteState?.canUnmute {
canUnmuteUpdated = true
}
if previousCallState.isVideoEnabled != state.isVideoEnabled || (state.isVideoEnabled && networkStateUpdated) || canUnmuteUpdated {
strongSelf.animatingButtonsSwap = true
animate = true
}
}
strongSelf.callState = state
strongSelf.mainStageNode.callState = state
if let muteState = state.muteState, !muteState.canUnmute {
if strongSelf.pushingToTalk {
strongSelf.pushingToTalk = false
strongSelf.actionButton.pressing = false
strongSelf.actionButton.isUserInteractionEnabled = false
strongSelf.actionButton.isUserInteractionEnabled = true
}
}
}
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: (callMembers?.participants ?? [], callMembers?.loadMoreToken), invitedPeers: invitedPeers, speakingPeers: callMembers?.speakingParticipants ?? [])
let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers?.totalCount ?? 0)))
strongSelf.currentSubtitle = subtitle
if strongSelf.isScheduling {
strongSelf.optionsButton.isUserInteractionEnabled = false
strongSelf.optionsButton.alpha = 0.0
strongSelf.closeButton.isUserInteractionEnabled = false
strongSelf.closeButton.alpha = 0.0
strongSelf.panelButton.isUserInteractionEnabled = false
strongSelf.panelButton.alpha = 0.0
} else {
if let (layout, _) = strongSelf.validLayout {
if case .regular = layout.metrics.widthClass, !strongSelf.peerIdToEndpointId.isEmpty {
strongSelf.panelButton.isUserInteractionEnabled = true
} else {
strongSelf.panelButton.isUserInteractionEnabled = false
}
}
if let callState = strongSelf.callState, callState.canManageCall {
strongSelf.optionsButton.isUserInteractionEnabled = true
} else if displayAsPeers.count > 1 {
strongSelf.optionsButton.isUserInteractionEnabled = true
} else {
strongSelf.optionsButton.isUserInteractionEnabled = true
}
}
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: animate ? .animated(duration: 0.4, curve: .spring) : .immediate)
}
})
let titleAndRecording: Signal<(String?, Bool), NoError> = self.call.state
|> map { state -> (String?, Bool) in
return (state.title, state.recordingStartTimestamp != nil)
}
self.peerViewDisposable = combineLatest(queue: Queue.mainQueue(), self.context.account.viewTracker.peerView(self.call.peerId), titleAndRecording).start(next: { [weak self] view, titleAndRecording in
guard let strongSelf = self else {
return
}
let (title, isRecording) = titleAndRecording
if let peer = peerViewMainPeer(view) {
strongSelf.peer = peer
strongSelf.currentTitleIsCustom = title != nil
strongSelf.currentTitle = title ?? peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)
strongSelf.updateTitle(transition: .immediate)
strongSelf.titleNode.isRecording = isRecording
}
if !strongSelf.didSetDataReady {
strongSelf.didSetDataReady = true
strongSelf.updateMembers()
strongSelf.controller?.dataReady.set(true)
}
})
self.audioOutputStateDisposable = (self.call.audioOutputState
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
let wasEmpty = strongSelf.audioOutputState == nil
strongSelf.audioOutputState = state
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
}
if wasEmpty {
strongSelf.controller?.audioOutputStateReady.set(true)
}
})
self.audioLevelsDisposable = (self.call.audioLevels
|> deliverOnMainQueue).start(next: { [weak self] levels in
guard let strongSelf = self else {
return
}
var levels = levels
if strongSelf.effectiveMuteState != nil {
levels = levels.filter { $0.0 != strongSelf.callState?.myPeerId }
}
var maxLevelWithVideo: (PeerId, Float)?
for (peerId, source, level, hasSpeech) in levels {
let hasVideo = strongSelf.peerIdToEndpointId[peerId] != nil
if hasSpeech && source != 0 && hasVideo {
if let (_, currentLevel) = maxLevelWithVideo {
if currentLevel < level {
maxLevelWithVideo = (peerId, level)
}
} else {
maxLevelWithVideo = (peerId, level)
}
}
}
if maxLevelWithVideo == nil {
if let (peerId, _, _) = strongSelf.currentDominantSpeaker {
maxLevelWithVideo = (peerId, 0.0)
} else if strongSelf.peerIdToEndpointId.count > 0 {
for entry in strongSelf.currentFullscreenEntries {
if case let .peer(peerEntry, _) = entry {
if let _ = peerEntry.effectiveVideoEndpointId {
maxLevelWithVideo = (peerEntry.peer.id, 0.0)
break
}
}
}
}
}
if case .fullscreen = strongSelf.displayMode, !strongSelf.mainStageNode.animating && !strongSelf.animatingExpansion {
if let (peerId, _) = maxLevelWithVideo {
if let (currentPeerId, _, timestamp) = strongSelf.currentDominantSpeaker {
if CACurrentMediaTime() - timestamp > 2.5 && peerId != currentPeerId {
strongSelf.currentDominantSpeaker = (peerId, nil, CACurrentMediaTime())
strongSelf.updateMainVideo(waitForFullSize: true)
}
}
}
}
strongSelf.itemInteraction?.updateAudioLevels(levels)
})
self.myAudioLevelDisposable = (self.call.myAudioLevel
|> deliverOnMainQueue).start(next: { [weak self] level in
guard let strongSelf = self else {
return
}
var effectiveLevel: Float = 0.0
if let state = strongSelf.callState, state.muteState == nil || strongSelf.pushingToTalk {
effectiveLevel = level
} else if level > 0.1 {
effectiveLevel = level * 0.5
}
strongSelf.actionButton.updateLevel(CGFloat(effectiveLevel))
})
self.isSpeakingDisposable = (self.call.isSpeaking
|> deliverOnMainQueue).start(next: { [weak self] isSpeaking in
guard let strongSelf = self else {
return
}
if let state = strongSelf.callState, state.muteState == nil || strongSelf.pushingToTalk {
strongSelf.displayUnmuteTooltipTimer?.invalidate()
strongSelf.displayUnmuteTooltipTimer = nil
strongSelf.dismissUnmuteTooltipTimer?.invalidate()
strongSelf.dismissUnmuteTooltipTimer = nil
} else {
if isSpeaking {
var shouldDisplayTooltip = false
if let previousTimstamp = strongSelf.lastUnmuteTooltipDisplayTimestamp, CACurrentMediaTime() > previousTimstamp + 45.0 {
shouldDisplayTooltip = true
} else if strongSelf.lastUnmuteTooltipDisplayTimestamp == nil {
shouldDisplayTooltip = true
}
if shouldDisplayTooltip {
strongSelf.dismissUnmuteTooltipTimer?.invalidate()
strongSelf.dismissUnmuteTooltipTimer = nil
if strongSelf.displayUnmuteTooltipTimer == nil {
let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.lastUnmuteTooltipDisplayTimestamp = CACurrentMediaTime()
strongSelf.displayUnmuteTooltip()
strongSelf.displayUnmuteTooltipTimer?.invalidate()
strongSelf.displayUnmuteTooltipTimer = nil
strongSelf.dismissUnmuteTooltipTimer?.invalidate()
strongSelf.dismissUnmuteTooltipTimer = nil
}, queue: Queue.mainQueue())
timer.start()
strongSelf.displayUnmuteTooltipTimer = timer
}
}
} else if strongSelf.dismissUnmuteTooltipTimer == nil && strongSelf.displayUnmuteTooltipTimer != nil {
let timer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.displayUnmuteTooltipTimer?.invalidate()
strongSelf.displayUnmuteTooltipTimer = nil
strongSelf.dismissUnmuteTooltipTimer?.invalidate()
strongSelf.dismissUnmuteTooltipTimer = nil
}, queue: Queue.mainQueue())
timer.start()
strongSelf.dismissUnmuteTooltipTimer = timer
}
}
})
self.leaveButton.addTarget(self, action: #selector(self.leavePressed), forControlEvents: .touchUpInside)
self.actionButton.addTarget(self, action: #selector(self.actionPressed), forControlEvents: .touchUpInside)
self.audioButton.addTarget(self, action: #selector(self.audioPressed), forControlEvents: .touchUpInside)
self.cameraButton.addTarget(self, action: #selector(self.cameraPressed), forControlEvents: .touchUpInside)
self.switchCameraButton.addTarget(self, action: #selector(self.switchCameraPressed), forControlEvents: .touchUpInside)
self.optionsButton.contextAction = { [weak self] sourceNode, gesture in
self?.openSettingsMenu(sourceNode: sourceNode, gesture: gesture)
}
self.optionsButton.addTarget(self, action: #selector(self.optionsPressed), forControlEvents: .touchUpInside)
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside)
self.panelButton.addTarget(self, action: #selector(self.panelPressed), forControlEvents: .touchUpInside)
self.actionButtonColorDisposable = (self.actionButton.outerColor
|> deliverOnMainQueue).start(next: { [weak self] normalColor, activeColor in
if let strongSelf = self {
let animated = strongSelf.currentNormalButtonColor != nil || strongSelf.currentActiveButtonColor == nil
strongSelf.currentNormalButtonColor = normalColor
strongSelf.currentActiveButtonColor = activeColor
strongSelf.updateButtons(transition: animated ? .animated(duration: 0.3, curve: .linear) : .immediate)
}
})
self.fullscreenListNode.updateFloatingHeaderOffset = { [weak self] _, _ in
guard let strongSelf = self else {
return
}
var visiblePeerIds = Set<PeerId>()
strongSelf.fullscreenListNode.forEachVisibleItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item {
if item.videoEndpointId == nil {
visiblePeerIds.insert(item.peer.id)
}
}
}
strongSelf.mainStageNode.update(visiblePeerIds: visiblePeerIds)
}
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
if let strongSelf = self {
strongSelf.currentContentOffset = offset
if !(strongSelf.animatingExpansion || strongSelf.animatingInsertion || strongSelf.animatingAppearance) && (strongSelf.panGestureArguments == nil || strongSelf.isExpanded) {
strongSelf.updateDecorationsLayout(transition: transition)
}
}
}
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
guard let strongSelf = self else {
return
}
if case let .known(value) = offset, value < 200.0 {
if let loadMoreToken = strongSelf.currentCallMembers?.1 {
strongSelf.currentLoadToken = loadMoreToken
strongSelf.call.loadMoreMembers(token: loadMoreToken)
}
}
}
self.memberEventsDisposable.set((self.call.memberEvents
|> deliverOnMainQueue).start(next: { [weak self] event in
guard let strongSelf = self else {
return
}
if event.joined {
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, text: strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return false })
}
}))
self.reconnectedAsEventsDisposable.set((self.call.reconnectedAsEvents
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let strongSelf = self else {
return
}
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), action: { _ in return false })
}))
self.stateVersionDisposable.set((self.call.stateVersion
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.callStateDidReset()
}))
self.titleNode.tapped = { [weak self] in
if let strongSelf = self, !strongSelf.isScheduling {
if strongSelf.callState?.canManageCall ?? false {
strongSelf.openTitleEditing()
} else if !strongSelf.titleNode.recordingIconNode.isHidden {
var hasTooltipAlready = false
strongSelf.controller?.forEachController { controller -> Bool in
if controller is TooltipScreen {
hasTooltipAlready = true
}
return true
}
if !hasTooltipAlready {
let location = strongSelf.titleNode.recordingIconNode.convert(strongSelf.titleNode.recordingIconNode.bounds, to: nil)
strongSelf.controller?.present(TooltipScreen(text: presentationData.strings.VoiceChat_RecordingInProgress, icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
return .dismiss(consume: true)
}), in: .window(.root))
}
}
}
}
self.scheduleCancelButton.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.dismissScheduled()
}
}
self.mainStageNode.controlsHidden = { [weak self] hidden in
if let strongSelf = self {
if hidden {
strongSelf.fullscreenListNode.alpha = 0.0
} else {
strongSelf.fullscreenListNode.alpha = 1.0
strongSelf.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
self.mainStageNode.tapped = { [weak self] in
if let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout, !strongSelf.animatingExpansion && !strongSelf.animatingMainStage && !strongSelf.mainStageNode.animating {
if case .regular = layout.metrics.widthClass {
strongSelf.panelHidden = !strongSelf.panelHidden
strongSelf.animatingExpansion = true
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
strongSelf.updateDecorationsLayout(transition: transition)
} else {
let effectiveDisplayMode = strongSelf.displayMode
let nextDisplayMode: DisplayMode
switch effectiveDisplayMode {
case .modal:
nextDisplayMode = effectiveDisplayMode
case let .fullscreen(controlsHidden):
if controlsHidden {
nextDisplayMode = .fullscreen(controlsHidden: false)
} else {
nextDisplayMode = .fullscreen(controlsHidden: true)
}
}
strongSelf.updateDisplayMode(nextDisplayMode)
}
}
}
self.mainStageNode.stopScreencast = { [weak self] in
if let strongSelf = self {
strongSelf.call.disableScreencast()
}
}
self.mainStageNode.back = { [weak self] in
if let strongSelf = self, !strongSelf.isPanning && !strongSelf.animatingExpansion && !strongSelf.mainStageNode.animating {
strongSelf.currentForcedSpeaker = nil
strongSelf.updateDisplayMode(.modal(isExpanded: true, isFilled: true), fromPan: true)
strongSelf.effectiveSpeaker = nil
}
}
self.mainStageNode.togglePin = { [weak self] in
if let strongSelf = self {
if let (peerId, videoEndpointId, _, _, _) = strongSelf.effectiveSpeaker {
if let _ = strongSelf.currentForcedSpeaker {
strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime())
strongSelf.currentForcedSpeaker = nil
} else {
strongSelf.currentForcedSpeaker = (peerId, videoEndpointId)
}
}
strongSelf.updateMembers()
}
}
self.mainStageNode.switchTo = { [weak self] peerId in
if let strongSelf = self, let interaction = strongSelf.itemInteraction {
interaction.switchToPeer(peerId, nil, false)
}
}
self.mainStageNode.getAudioLevel = { [weak self] peerId in
return self?.itemInteraction?.getAudioLevel(peerId) ?? .single(0.0)
}
self.mainStageNode.getVideo = { [weak self] endpointId, isMyPeer, completion in
if let strongSelf = self {
if isMyPeer {
if strongSelf.readyVideoEndpointIds.contains(endpointId) {
completion(strongSelf.itemInteraction?.getPeerVideo(endpointId, .mainstage))
} else {
strongSelf.myPeerVideoReadyDisposable.set((strongSelf.readyVideoEndpointIdsPromise.get()
|> filter { $0.contains(endpointId) }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self {
completion(strongSelf.itemInteraction?.getPeerVideo(endpointId, .mainstage))
}
}))
}
} else {
if let input = (strongSelf.call as! PresentationGroupCallImpl).video(endpointId: endpointId) {
if let videoView = strongSelf.videoRenderingContext.makeView(input: input, blur: false) {
completion(GroupVideoNode(videoView: videoView, backdropVideoView: strongSelf.videoRenderingContext.makeView(input: input, blur: true)))
}
}
/*strongSelf.call.makeIncomingVideoView(endpointId: endpointId, requestClone: GroupVideoNode.useBlurTransparency, completion: { videoView, backdropVideoView in
if let videoView = videoView {
completion(GroupVideoNode(videoView: videoView, backdropVideoView: backdropVideoView))
} else {
completion(nil)
}
})*/
}
}
}
self.applicationStateDisposable = (self.context.sharedContext.applicationBindings.applicationIsActive
|> deliverOnMainQueue).start(next: { [weak self] active in
guard let strongSelf = self else {
return
}
strongSelf.appIsActive = active
})
if self.context.sharedContext.immediateExperimentalUISettings.enableDebugDataDisplay {
self.statsDisposable = ((call as! PresentationGroupCallImpl).getStats()
|> deliverOnMainQueue
|> then(.complete() |> delay(1.0, queue: .mainQueue()))
|> restart).start(next: { [weak self] stats in
guard let strongSelf = self else {
return
}
for (endpointId, videoNode) in strongSelf.videoNodes {
if let incomingVideoStats = stats.incomingVideoStats[endpointId] {
videoNode.updateDebugInfo(text: "in: \(incomingVideoStats.receivingQuality)\n srv: \(incomingVideoStats.availableQuality)")
}
}
if let (_, maybeEndpointId, _, _, _) = strongSelf.mainStageNode.currentPeer, let endpointId = maybeEndpointId {
if let incomingVideoStats = stats.incomingVideoStats[endpointId] {
strongSelf.mainStageNode.currentVideoNode?.updateDebugInfo(text: "in: \(incomingVideoStats.receivingQuality)\n srv: \(incomingVideoStats.availableQuality)")
}
}
})
}
}
deinit {
self.presentationDataDisposable?.dispose()
self.peerViewDisposable?.dispose()
self.leaveDisposable.dispose()
self.isMutedDisposable?.dispose()
self.isNoiseSuppressionEnabledDisposable?.dispose()
self.callStateDisposable?.dispose()
self.audioOutputStateDisposable?.dispose()
self.memberStatesDisposable?.dispose()
self.audioLevelsDisposable?.dispose()
self.myAudioLevelDisposable?.dispose()
self.isSpeakingDisposable?.dispose()
self.inviteDisposable.dispose()
self.memberEventsDisposable.dispose()
self.reconnectedAsEventsDisposable.dispose()
self.stateVersionDisposable.dispose()
self.updateAvatarDisposable.dispose()
self.ignoreConnectingTimer?.invalidate()
self.readyVideoDisposables.dispose()
self.applicationStateDisposable?.dispose()
self.myPeerVideoReadyDisposable.dispose()
self.statsDisposable?.dispose()
}
private func openSettingsMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems()
if let controller = self.controller {
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: self.optionsButton.referenceNode)), items: items, reactionItems: [], gesture: gesture)
controller.presentInGlobalOverlay(contextController)
}
}
private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> {
guard let myPeerId = self.callState?.myPeerId else {
return .single([])
}
let canManageCall = self.callState?.canManageCall == true
let avatarSize = CGSize(width: 28.0, height: 28.0)
return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(self.call.peerId), self.inviteLinksPromise.get())
|> take(1)
|> deliverOnMainQueue
|> map { [weak self] peers, chatPeer, inviteLinks -> [ContextMenuItem] in
guard let strongSelf = self else {
return []
}
var items: [ContextMenuItem] = []
if peers.count > 1 {
for peer in peers {
if peer.peer.id == myPeerId {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, _ in
guard let strongSelf = self else {
return
}
c.setItems(strongSelf.contextMenuDisplayAsItems())
})))
items.append(.separator)
break
}
}
}
if let (availableOutputs, currentOutput) = strongSelf.audioOutputState, availableOutputs.count > 1 {
var currentOutputTitle = ""
for output in availableOutputs {
if output == currentOutput {
let title: String
switch output {
case .builtin:
title = UIDevice.current.model
case .speaker:
title = strongSelf.presentationData.strings.Call_AudioRouteSpeaker
case .headphones:
title = strongSelf.presentationData.strings.Call_AudioRouteHeadphones
case let .port(port):
title = port.name
}
currentOutputTitle = title
break
}
}
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_ContextAudio, textLayout: .secondLineWithValue(currentOutputTitle), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Audio"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
guard let strongSelf = self else {
return
}
c.setItems(strongSelf.contextMenuAudioItems())
})))
}
if canManageCall {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditTitle, icon: { theme -> UIImage? in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
guard let strongSelf = self else {
return
}
strongSelf.openTitleEditing()
})))
var hasPermissions = true
if let chatPeer = chatPeer as? TelegramChannel {
if case .broadcast = chatPeer.info {
hasPermissions = false
} else if chatPeer.flags.contains(.isGigagroup) {
hasPermissions = false
}
}
if hasPermissions {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
guard let strongSelf = self else {
return
}
c.setItems(strongSelf.contextMenuPermissionItems())
})))
}
}
if let inviteLinks = inviteLinks {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
self?.presentShare(inviteLinks)
})))
}
let isScheduled = strongSelf.isScheduled
let canSpeak: Bool
if let callState = strongSelf.callState {
if let muteState = callState.muteState {
canSpeak = muteState.canUnmute
} else {
canSpeak = true
}
} else {
canSpeak = false
}
if !isScheduled && canSpeak {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_NoiseSuppression, textColor: .primary, textLayout: .secondLineWithValue(strongSelf.isNoiseSuppressionEnabled ? strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionEnabled : strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionDisabled), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
if let strongSelf = self {
strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled)
}
})))
}
if let callState = strongSelf.callState, callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) {
if #available(iOS 12.0, *) {
if strongSelf.call.hasScreencast {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StopScreenSharing, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
self?.call.disableScreencast()
})))
} else {
items.append(.custom(VoiceChatShareScreenContextItem(context: strongSelf.context, text: strongSelf.presentationData.strings.VoiceChat_ShareScreen, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor)
}, action: { _, _ in }), false))
}
}
}
if canManageCall {
if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp {
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StopRecordingStop, action: {
if let strongSelf = self {
strongSelf.call.setShouldBeRecording(false, title: nil, videoOrientation: nil)
strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: strongSelf.presentationData.strings.VoiceChat_RecordingSaved), action: { [weak self] value in
if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
let context = strongSelf.context
strongSelf.controller?.dismiss(completion: {
Queue.mainQueue().justDispatch {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(context.account.peerId), keepStack: .always, purposefulAction: {}, peekData: nil))
}
})
return true
}
return false
})
}
})])
self?.controller?.present(alertController, in: .window(.root))
}), false))
} else {
if strongSelf.callState?.scheduleTimestamp == nil {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
let controller = VoiceChatRecordingSetupController(context: strongSelf.context, completion: { [weak self] videoOrientation in
if let strongSelf = self {
strongSelf.call.setShouldBeRecording(true, title: "", videoOrientation: videoOrientation)
strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
strongSelf.call.playTone(.recordingStarted)
}
})
// let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in
// if let strongSelf = self, let title = title {
// strongSelf.call.setShouldBeRecording(true, title: title)
//
// strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
// strongSelf.call.playTone(.recordingStarted)
// }
// })
self?.controller?.present(controller, in: .window(.root))
})))
}
}
}
if canManageCall {
items.append(.action(ContextMenuActionItem(text: isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelVoiceChat : strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
let action: () -> Void = {
guard let strongSelf = self else {
return
}
let _ = (strongSelf.call.leave(terminateIfPossible: true)
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(completed: {
self?.controller?.dismiss()
})
}
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationTitle : strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle, text: isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationText : strongSelf.presentationData.strings.VoiceChat_EndConfirmationText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationEnd : strongSelf.presentationData.strings.VoiceChat_EndConfirmationEnd, action: {
action()
})])
strongSelf.controller?.present(alertController, in: .window(.root))
})))
} else {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_LeaveVoiceChat, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
let _ = (strongSelf.call.leave(terminateIfPossible: false)
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(completed: {
self?.controller?.dismiss()
})
})))
}
return items
}
}
private func contextMenuAudioItems() -> Signal<[ContextMenuItem], NoError> {
guard let (availableOutputs, currentOutput) = self.audioOutputState else {
return .single([])
}
var items: [ContextMenuItem] = []
for output in availableOutputs {
let title: String
switch output {
case .builtin:
title = UIDevice.current.model
case .speaker:
title = self.presentationData.strings.Call_AudioRouteSpeaker
case .headphones:
title = self.presentationData.strings.Call_AudioRouteHeadphones
case let .port(port):
title = port.name
}
items.append(.action(ContextMenuActionItem(text: title, icon: { theme in
if output == currentOutput {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor)
} else {
return nil
}
}, action: { [weak self] _, f in
f(.default)
self?.call.setCurrentAudioOutput(output)
})))
}
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] (c, _) in
guard let strongSelf = self else {
return
}
c.setItems(strongSelf.contextMenuMainItems())
})))
return .single(items)
}
private func contextMenuDisplayAsItems() -> Signal<[ContextMenuItem], NoError> {
guard let myPeerId = self.callState?.myPeerId else {
return .single([])
}
let avatarSize = CGSize(width: 28.0, height: 28.0)
let darkTheme = self.darkTheme
return self.displayAsPeersPromise.get()
|> take(1)
|> map { [weak self] peers -> [ContextMenuItem] in
guard let strongSelf = self else {
return []
}
var items: [ContextMenuItem] = []
var isGroup = false
for peer in peers {
if peer.peer is TelegramGroup {
isGroup = true
break
} else if let peer = peer.peer as? TelegramChannel, case .group = peer.info {
isGroup = true
break
}
}
items.append(.custom(VoiceChatInfoContextItem(text: isGroup ? strongSelf.presentationData.strings.VoiceChat_DisplayAsInfoGroup : strongSelf.presentationData.strings.VoiceChat_DisplayAsInfo, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Accounts"), color: theme.actionSheet.primaryTextColor)
}), true))
for peer in peers {
var subtitle: String?
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
subtitle = strongSelf.presentationData.strings.VoiceChat_PersonalAccount
} else if let subscribers = peer.subscribers {
if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info {
subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers)
} else {
subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(subscribers)
}
}
let isSelected = peer.peer.id == myPeerId
let extendedAvatarSize = CGSize(width: 35.0, height: 35.0)
let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)
|> map { image -> UIImage? in
if isSelected, let image = image {
return generateImage(extendedAvatarSize, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height))
let lineWidth = 1.0 + UIScreenPixel
context.setLineWidth(lineWidth)
context.setStrokeColor(darkTheme.actionSheet.controlAccentColor.cgColor)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
})
} else {
return image
}
}
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { _, f in
f(.default)
guard let strongSelf = self else {
return
}
if peer.peer.id != myPeerId {
strongSelf.call.reconnect(as: peer.peer.id)
}
})))
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
items.append(.separator)
}
}
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, action: { (c, _) in
guard let strongSelf = self else {
return
}
c.setItems(strongSelf.contextMenuMainItems())
})))
return items
}
}
private func contextMenuPermissionItems() -> Signal<[ContextMenuItem], NoError> {
var items: [ContextMenuItem] = []
if let callState = self.callState, callState.canManageCall, let defaultParticipantMuteState = callState.defaultParticipantMuteState {
let isMuted = defaultParticipantMuteState == .muted
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in
if isMuted {
return nil
} else {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor)
}
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: false)
})))
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.VoiceChat_SpeakPermissionAdmin, icon: { theme in
if !isMuted {
return nil
} else {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor)
}
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let strongSelf = self else {
return
}
strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: true)
})))
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] (c, _) in
guard let strongSelf = self else {
return
}
c.setItems(strongSelf.contextMenuMainItems())
})))
}
return .single(items)
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
self.view.disablesInteractiveModalDismiss = true
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.actionButtonPressGesture(_:)))
longTapRecognizer.minimumPressDuration = 0.001
longTapRecognizer.delegate = self
self.actionButton.view.addGestureRecognizer(longTapRecognizer)
let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(panRecognizer)
if self.isScheduling {
self.setupSchedulePickerView()
self.updateScheduleButtonTitle()
}
}
private func updateSchedulePickerLimits() {
let timeZone = TimeZone(secondsFromGMT: 0)!
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = timeZone
let currentDate = Date()
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate)
components.second = 0
let roundedDate = calendar.date(from: components)!
let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: roundedDate)
let minute = components.minute ?? 0
components.minute = 0
let roundedToHourDate = calendar.date(from: components)!
components.hour = 0
let roundedToMidnightDate = calendar.date(from: components)!
let nextTwoHourDate = calendar.date(byAdding: .hour, value: minute > 30 ? 4 : 3, to: roundedToHourDate)
let maxDate = calendar.date(byAdding: .day, value: 8, to: roundedToMidnightDate)
if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) {
self.pickerView?.maximumDate = date
}
if let next1MinDate = next1MinDate, let nextTwoHourDate = nextTwoHourDate {
self.pickerView?.minimumDate = next1MinDate
self.pickerView?.maximumDate = maxDate
self.pickerView?.date = nextTwoHourDate
}
}
private func setupSchedulePickerView() {
var currentDate: Date?
if let pickerView = self.pickerView {
currentDate = pickerView.date
pickerView.removeFromSuperview()
}
let textColor = UIColor.white
UILabel.setDateLabel(textColor)
let pickerView = UIDatePicker()
pickerView.timeZone = TimeZone(secondsFromGMT: 0)
pickerView.datePickerMode = .countDownTimer
pickerView.datePickerMode = .dateAndTime
pickerView.locale = Locale.current
pickerView.timeZone = TimeZone.current
pickerView.minuteInterval = 1
self.contentContainer.view.addSubview(pickerView)
pickerView.addTarget(self, action: #selector(self.scheduleDatePickerUpdated), for: .valueChanged)
if #available(iOS 13.4, *) {
pickerView.preferredDatePickerStyle = .wheels
}
pickerView.setValue(textColor, forKey: "textColor")
self.pickerView = pickerView
self.updateSchedulePickerLimits()
if let currentDate = currentDate {
pickerView.date = currentDate
}
}
private let calendar = Calendar(identifier: .gregorian)
private func updateScheduleButtonTitle() {
guard let date = self.pickerView?.date else {
return
}
let calendar = Calendar(identifier: .gregorian)
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let timestamp = Int32(date.timeIntervalSince1970)
let time = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: self.presentationData.dateTimeFormat)
let buttonTitle: String
if calendar.isDateInToday(date) {
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleToday(time).string
} else if calendar.isDateInTomorrow(date) {
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleTomorrow(time).string
} else {
buttonTitle = self.presentationData.strings.ScheduleVoiceChat_ScheduleOn(self.dateFormatter.string(from: date), time).string
}
self.scheduleButtonTitle = buttonTitle
let delta = timestamp - currentTimestamp
var isGroup = true
if let peer = self.peer as? TelegramChannel, case .broadcast = peer.info {
isGroup = false
}
let intervalString = scheduledTimeIntervalString(strings: self.presentationData.strings, value: max(60, delta))
self.scheduleTextNode.attributedText = NSAttributedString(string: isGroup ? self.presentationData.strings.ScheduleVoiceChat_GroupText(intervalString).string : self.presentationData.strings.ScheduleVoiceChat_ChannelText(intervalString).string, font: Font.regular(14.0), textColor: UIColor(rgb: 0x8e8e93))
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
}
@objc private func scheduleDatePickerUpdated() {
self.updateScheduleButtonTitle()
}
private func schedule() {
if let date = self.pickerView?.date, date > Date() {
self.call.schedule(timestamp: Int32(date.timeIntervalSince1970))
self.isScheduling = false
self.transitionToScheduled()
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
}
}
private func dismissScheduled() {
self.leaveDisposable.set((self.call.leave(terminateIfPossible: true)
|> deliverOnMainQueue).start(completed: { [weak self] in
self?.controller?.dismiss(closing: true)
}))
}
private func transitionToScheduled() {
let springDuration: Double = 0.6
let springDamping: CGFloat = 100.0
self.optionsButton.alpha = 1.0
self.optionsButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.optionsButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping)
self.optionsButton.isUserInteractionEnabled = true
self.closeButton.alpha = 1.0
self.closeButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.closeButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping)
self.closeButton.isUserInteractionEnabled = true
self.audioButton.alpha = 1.0
self.audioButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.audioButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping)
self.audioButton.isUserInteractionEnabled = true
self.leaveButton.alpha = 1.0
self.leaveButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.leaveButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, damping: springDamping)
self.leaveButton.isUserInteractionEnabled = true
self.scheduleCancelButton.alpha = 0.0
self.scheduleCancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
self.scheduleCancelButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 26.0), duration: 0.2, removeOnCompletion: false, additive: true)
self.actionButton.titleLabel.layer.animatePosition(from: CGPoint(x: 0.0, y: -26.0), to: CGPoint(), duration: 0.2, additive: true)
if let pickerView = self.pickerView {
self.pickerView = nil
pickerView.alpha = 0.0
pickerView.layer.animateScale(from: 1.0, to: 0.25, duration: 0.15, removeOnCompletion: false)
pickerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak pickerView] _ in
pickerView?.removeFromSuperview()
})
pickerView.isUserInteractionEnabled = false
}
self.timerNode.isHidden = false
self.timerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.timerNode.animateIn()
self.scheduleTextNode.alpha = 0.0
self.scheduleTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
self.updateTitle(slide: true, transition: .animated(duration: 0.2, curve: .easeInOut))
}
private func transitionToCall() {
self.updateDecorationsColors()
self.listNode.alpha = 1.0
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.listNode.isUserInteractionEnabled = true
self.timerNode.alpha = 0.0
self.timerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.timerNode.isHidden = true
})
if self.audioButton.isHidden {
self.audioButton.isHidden = false
self.audioButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.audioButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, damping: 100.0)
}
self.updateTitle(transition: .animated(duration: 0.2, curve: .easeInOut))
}
@objc private func optionsPressed() {
self.optionsButton.play()
self.optionsButton.contextAction?(self.optionsButton.containerNode, nil)
}
@objc private func closePressed() {
self.controller?.dismiss(closing: false)
self.controller?.dismissAllTooltips()
}
@objc private func panelPressed() {
guard let (layout, navigationHeight) = self.validLayout, !self.animatingExpansion && !self.animatingMainStage && !self.mainStageNode.animating else {
return
}
self.panelHidden = !self.panelHidden
self.animatingExpansion = true
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
self.updateDecorationsLayout(transition: transition)
}
@objc private func leavePressed() {
self.hapticFeedback.impact(.light)
self.controller?.dismissAllTooltips()
if let callState = self.callState, callState.canManageCall {
let action: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.leaveDisposable.set((strongSelf.call.leave(terminateIfPossible: true)
|> deliverOnMainQueue).start(completed: {
self?.controller?.dismiss()
}))
}
let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: self.darkTheme))
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: self.presentationData.strings.VoiceChat_LeaveConfirmation))
items.append(ActionSheetButtonItem(title: self.isScheduled ? self.presentationData.strings.VoiceChat_LeaveAndCancelVoiceChat : self.presentationData.strings.VoiceChat_LeaveAndEndVoiceChat, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
if let (members, _) = strongSelf.currentCallMembers, members.count >= 10 || true {
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: strongSelf.isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationTitle : strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle, text: strongSelf.isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationText : strongSelf.presentationData.strings.VoiceChat_EndConfirmationText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationEnd : strongSelf.presentationData.strings.VoiceChat_EndConfirmationEnd, action: {
action()
})])
strongSelf.controller?.present(alertController, in: .window(.root))
} else {
action()
}
}
}))
items.append(ActionSheetButtonItem(title: self.presentationData.strings.VoiceChat_LeaveVoiceChat, color: .accent, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
strongSelf.leaveDisposable.set((strongSelf.call.leave(terminateIfPossible: false)
|> deliverOnMainQueue).start(completed: { [weak self] in
self?.controller?.dismiss(closing: true)
}))
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.controller?.present(actionSheet, in: .window(.root))
} else {
self.leaveDisposable.set((self.call.leave(terminateIfPossible: false)
|> deliverOnMainQueue).start(completed: { [weak self] in
self?.controller?.dismiss(closing: true)
}))
}
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if self.isScheduling {
self.dismissScheduled()
} else {
self.controller?.dismiss(closing: false)
self.controller?.dismissAllTooltips()
}
}
}
private func presentUndoOverlay(content: UndoOverlayContent, action: @escaping (UndoOverlayAction) -> Bool) {
var animateInAsReplacement = false
self.controller?.forEachController { c in
if let c = c as? UndoOverlayController {
animateInAsReplacement = true
c.dismiss()
}
return true
}
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: action), in: .current)
}
private func presentShare(_ inviteLinks: GroupCallInviteLinks) {
let formatSendTitle: (String) -> String = { string in
var string = string
if string.contains("[") && string.contains("]") {
if let startIndex = string.firstIndex(of: "["), let endIndex = string.firstIndex(of: "]") {
string.removeSubrange(startIndex ... endIndex)
}
} else {
string = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,."))
}
return string
}
let _ = (self.context.account.postbox.loadedPeerWithId(self.call.peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self {
var inviteLinks = inviteLinks
if let peer = peer as? TelegramChannel, case .group = peer.info, !peer.flags.contains(.isGigagroup), !(peer.addressName ?? "").isEmpty, let callState = strongSelf.callState, let defaultParticipantMuteState = callState.defaultParticipantMuteState {
let isMuted = defaultParticipantMuteState == .muted
if !isMuted {
inviteLinks = GroupCallInviteLinks(listenerLink: inviteLinks.listenerLink, speakerLink: nil)
}
}
let presentationData = strongSelf.presentationData
var segmentedValues: [ShareControllerSegmentedValue]?
if let speakerLink = inviteLinks.speakerLink {
segmentedValues = [ShareControllerSegmentedValue(title: presentationData.strings.VoiceChat_InviteLink_Speaker, subject: .url(speakerLink), actionTitle: presentationData.strings.VoiceChat_InviteLink_CopySpeakerLink, formatSendTitle: { count in
return formatSendTitle(presentationData.strings.VoiceChat_InviteLink_InviteSpeakers(Int32(count)))
}), ShareControllerSegmentedValue(title: presentationData.strings.VoiceChat_InviteLink_Listener, subject: .url(inviteLinks.listenerLink), actionTitle: presentationData.strings.VoiceChat_InviteLink_CopyListenerLink, formatSendTitle: { count in
return formatSendTitle(presentationData.strings.VoiceChat_InviteLink_InviteListeners(Int32(count)))
})]
}
let shareController = ShareController(context: strongSelf.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: strongSelf.darkTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink)
shareController.completed = { [weak self] peerIds in
if let strongSelf = self {
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in
var peers: [Peer] = []
for peerId in peerIds {
if let peer = transaction.getPeer(peerId) {
peers.append(peer)
}
}
return peers
} |> deliverOnMainQueue).start(next: { [weak self] peers in
if let strongSelf = self {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let text: String
var isSavedMessages = false
if peers.count == 1, let peer = peers.first {
isSavedMessages = peer.id == strongSelf.context.account.peerId
let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.VoiceChat_ForwardTooltip_Chat(peerName).string
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.VoiceChat_ForwardTooltip_TwoChats(firstPeerName, secondPeerName).string
} else if let peer = peers.first {
let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.VoiceChat_ForwardTooltip_ManyChats(peerName, "\(peers.count - 1)").string
} else {
text = ""
}
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: isSavedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
}
})
}
}
shareController.actionCompleted = { [weak self] in
if let strongSelf = self {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}
}
strongSelf.controller?.present(shareController, in: .window(.root))
}
})
}
private var actionButtonPressTimer: SwiftSignalKit.Timer?
private var actionButtonPressedTimestamp: Double?
private func startActionButtonPressTimer() {
self.actionButtonPressTimer?.invalidate()
let pressTimer = SwiftSignalKit.Timer(timeout: 0.185, repeat: false, completion: { [weak self] in
self?.actionButtonPressedTimestamp = CACurrentMediaTime()
self?.actionButtonPressTimerFired()
self?.actionButtonPressTimer = nil
}, queue: Queue.mainQueue())
self.actionButtonPressTimer = pressTimer
pressTimer.start()
}
private func stopActionButtonPressTimer() {
self.actionButtonPressTimer?.invalidate()
self.actionButtonPressTimer = nil
}
private func actionButtonPressTimerFired() {
guard let callState = self.callState else {
return
}
if callState.muteState != nil {
self.pushingToTalk = true
self.call.setIsMuted(action: .muted(isPushToTalkActive: true))
}
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
self.updateMembers()
}
@objc private func actionButtonPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer) {
guard let callState = self.callState else {
return
}
if case .connecting = callState.networkState, callState.scheduleTimestamp == nil && !self.isScheduling {
return
}
if callState.scheduleTimestamp != nil || self.isScheduling {
switch gestureRecognizer.state {
case .began:
self.actionButton.pressing = true
self.hapticFeedback.impact(.light)
case .ended, .cancelled:
self.actionButton.pressing = false
let location = gestureRecognizer.location(in: self.actionButton.view)
if self.actionButton.hitTest(location, with: nil) != nil {
if self.isScheduling {
self.schedule()
} else if callState.canManageCall {
self.call.startScheduled()
} else {
if !callState.subscribedToScheduled {
let location = self.actionButton.view.convert(self.actionButton.bounds, to: self.view).center
let point = CGRect(origin: CGPoint(x: location.x - 5.0, y: location.y - 5.0 - 68.0), size: CGSize(width: 10.0, height: 10.0))
self.controller?.present(TooltipScreen(text: self.presentationData.strings.VoiceChat_ReminderNotify, style: .gradient(UIColor(rgb: 0x262c5a), UIColor(rgb: 0x5d2835)), icon: nil, location: .point(point, .bottom), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
return .dismiss(consume: false)
}), in: .window(.root))
}
self.call.toggleScheduledSubscription(!callState.subscribedToScheduled)
}
}
default:
break
}
return
}
if let muteState = callState.muteState {
if !muteState.canUnmute {
switch gestureRecognizer.state {
case .began:
self.actionButton.pressing = true
self.hapticFeedback.impact(.light)
case .ended, .cancelled:
self.actionButton.pressing = false
let location = gestureRecognizer.location(in: self.actionButton.view)
if self.actionButton.hitTest(location, with: nil) != nil {
self.call.raiseHand()
self.actionButton.playAnimation()
}
default:
break
}
return
}
}
switch gestureRecognizer.state {
case .began:
self.actionButton.pressing = true
self.hapticFeedback.impact(.light)
self.actionButtonPressedTimestamp = nil
self.startActionButtonPressTimer()
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
case .ended, .cancelled:
if self.actionButtonPressTimer != nil {
self.pushingToTalk = false
self.actionButton.pressing = false
self.stopActionButtonPressTimer()
self.call.toggleIsMuted()
} else {
self.hapticFeedback.impact(.light)
if self.pushingToTalk, let timestamp = self.actionButtonPressedTimestamp, CACurrentMediaTime() < timestamp + 0.5 {
self.pushingToTalk = false
self.temporaryPushingToTalk = true
self.call.setIsMuted(action: .unmuted)
Queue.mainQueue().after(0.1) {
self.temporaryPushingToTalk = false
self.actionButton.pressing = false
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
}
} else {
self.pushingToTalk = false
self.actionButton.pressing = false
self.call.setIsMuted(action: .muted(isPushToTalkActive: false))
}
}
if let callState = self.callState {
self.itemInteraction?.updateAudioLevels([(callState.myPeerId, 0, 0.0, false)], reset: true)
}
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
self.updateMembers()
default:
break
}
}
@objc private func actionPressed() {
if self.isScheduling {
self.schedule()
}
}
@objc private func audioPressed() {
self.hapticFeedback.impact(.light)
if let _ = self.callState?.scheduleTimestamp {
if let callState = self.callState, let peer = self.peer, !callState.canManageCall && (peer.addressName?.isEmpty ?? true) {
return
}
let _ = (self.inviteLinksPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] inviteLinks in
guard let strongSelf = self else {
return
}
let callPeerId = strongSelf.call.peerId
let _ = (strongSelf.context.account.postbox.transaction { transaction -> GroupCallInviteLinks? in
if let inviteLinks = inviteLinks {
return inviteLinks
} else if let peer = transaction.getPeer(callPeerId), let addressName = peer.addressName, !addressName.isEmpty {
return GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)?voicechat", speakerLink: nil)
} else if let cachedData = transaction.getPeerCachedData(peerId: callPeerId) {
if let cachedData = cachedData as? CachedChannelData, let link = cachedData.exportedInvitation?.link {
return GroupCallInviteLinks(listenerLink: link, speakerLink: nil)
} else if let cachedData = cachedData as? CachedGroupData, let link = cachedData.exportedInvitation?.link {
return GroupCallInviteLinks(listenerLink: link, speakerLink: nil)
}
}
return nil
}
|> deliverOnMainQueue).start(next: { links in
guard let strongSelf = self else {
return
}
if let links = links {
strongSelf.presentShare(links)
}
})
})
return
}
guard let (availableOutputs, currentOutput) = self.audioOutputState else {
return
}
guard availableOutputs.count >= 2 else {
return
}
if availableOutputs.count == 2 {
for output in availableOutputs {
if output != currentOutput {
self.call.setCurrentAudioOutput(output)
break
}
}
} else {
let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: self.darkTheme))
var items: [ActionSheetItem] = []
for output in availableOutputs {
let title: String
var icon: UIImage?
switch output {
case .builtin:
title = UIDevice.current.model
case .speaker:
title = self.presentationData.strings.Call_AudioRouteSpeaker
icon = generateScaledImage(image: UIImage(bundleImageName: "Call/CallSpeakerButton"), size: CGSize(width: 48.0, height: 48.0), opaque: false)
case .headphones:
title = self.presentationData.strings.Call_AudioRouteHeadphones
case let .port(port):
title = port.name
if port.type == .bluetooth {
var image = UIImage(bundleImageName: "Call/CallBluetoothButton")
let portName = port.name.lowercased()
if portName.contains("airpods max") {
image = UIImage(bundleImageName: "Call/CallAirpodsMaxButton")
} else if portName.contains("airpods pro") {
image = UIImage(bundleImageName: "Call/CallAirpodsProButton")
} else if portName.contains("airpods") {
image = UIImage(bundleImageName: "Call/CallAirpodsButton")
}
icon = generateScaledImage(image: image, size: CGSize(width: 48.0, height: 48.0), opaque: false)
}
}
items.append(CallRouteActionSheetItem(title: title, icon: icon, selected: output == currentOutput, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
self?.call.setCurrentAudioOutput(output)
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Call_AudioRouteHide, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.controller?.present(actionSheet, in: .window(.calls))
}
}
@objc private func cameraPressed() {
self.hapticFeedback.impact(.light)
if self.call.hasVideo {
self.call.disableVideo()
if let (layout, navigationHeight) = self.validLayout {
self.animatingButtonsSwap = true
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
} else {
DeviceAccess.authorizeAccess(to: .camera(.videoCall), onlyCheck: true, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), present: { [weak self] c, a in
self?.controller?.present(c, in: .window(.root), with: a)
}, openSettings: { [weak self] in
self?.context.sharedContext.applicationBindings.openSettings()
}, _: { [weak self] ready in
guard let strongSelf = self, ready else {
return
}
var isFrontCamera = true
let videoCapturer = OngoingCallVideoCapturer()
let input = videoCapturer.video()
if let videoView = strongSelf.videoRenderingContext.makeView(input: input, blur: false) {
videoView.updateIsEnabled(true)
let cameraNode = GroupVideoNode(videoView: videoView, backdropVideoView: nil)
let controller = VoiceChatCameraPreviewController(sharedContext: strongSelf.context.sharedContext, cameraNode: cameraNode, shareCamera: { [weak self] _, unmuted in
if let strongSelf = self {
strongSelf.call.setIsMuted(action: unmuted ? .unmuted : .muted(isPushToTalkActive: false))
(strongSelf.call as! PresentationGroupCallImpl).requestVideo(capturer: videoCapturer, useFrontCamera: isFrontCamera)
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.animatingButtonsSwap = true
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
}
}, switchCamera: {
Queue.mainQueue().after(0.1) {
isFrontCamera = !isFrontCamera
videoCapturer.switchVideoInput(isFront: isFrontCamera)
}
})
strongSelf.controller?.present(controller, in: .window(.root))
}
})
}
}
@objc private func switchCameraPressed() {
self.hapticFeedback.impact(.light)
Queue.mainQueue().after(0.1) {
self.call.switchVideoCamera()
}
if let callState = self.callState {
for entry in self.currentFullscreenEntries {
if case let .peer(peerEntry, _) = entry {
if peerEntry.peer.id == callState.myPeerId {
if let videoEndpointId = peerEntry.videoEndpointId, let videoNode = self.videoNodes[videoEndpointId] {
videoNode.flip(withBackground: false)
}
break
}
}
}
}
self.mainStageNode.flipVideoIfNeeded()
let springDuration: Double = 0.7
let springDamping: CGFloat = 100.0
self.switchCameraButton.isUserInteractionEnabled = false
self.switchCameraButton.layer.animateSpring(from: 0.0 as NSNumber, to: CGFloat.pi as NSNumber, keyPath: "transform.rotation.z", duration: springDuration, damping: springDamping, completion: { [weak self] _ in
self?.switchCameraButton.isUserInteractionEnabled = true
})
}
private var isLandscape: Bool {
if let (layout, _) = self.validLayout, layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass {
return true
} else {
return false
}
}
private var effectiveBottomAreaHeight: CGFloat {
if let (layout, _) = self.validLayout, case .regular = layout.metrics.widthClass {
return bottomAreaHeight
}
switch self.displayMode {
case .modal:
return bottomAreaHeight
case let .fullscreen(controlsHidden):
return controlsHidden ? 0.0 : fullscreenBottomAreaHeight
}
}
private var isFullscreen: Bool {
switch self.displayMode {
case .fullscreen(_), .modal(_, true):
return true
default:
return false
}
}
private func updateDecorationsLayout(transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) {
guard let (layout, _) = self.validLayout else {
return
}
let isLandscape = self.isLandscape
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
let listTopInset = isLandscape ? topPanelHeight : layoutTopInset + topPanelHeight
let bottomPanelHeight = isLandscape ? layout.intrinsicInsets.bottom : bottomAreaHeight + layout.intrinsicInsets.bottom
let size = layout.size
let contentWidth: CGFloat
var contentLeftInset: CGFloat = 0.0
var forceUpdate = false
if case .regular = layout.metrics.widthClass {
contentWidth = max(320.0, min(375.0, floor(size.width * 0.3)))
if self.peerIdToEndpointId.isEmpty {
contentLeftInset = 0.0
} else {
contentLeftInset = self.panelHidden ? layout.size.width : layout.size.width - contentWidth
}
forceUpdate = true
} else {
contentWidth = isLandscape ? min(530.0, size.width - 210.0) : size.width
}
let listSize = CGSize(width: contentWidth, height: layout.size.height - listTopInset - bottomPanelHeight + bottomGradientHeight)
let topInset: CGFloat
if let (panInitialTopInset, panOffset) = self.panGestureArguments {
if self.isExpanded {
topInset = min(self.topInset ?? listSize.height, panInitialTopInset + max(0.0, panOffset))
} else {
topInset = max(0.0, panInitialTopInset + min(0.0, panOffset))
}
} else if case .regular = layout.metrics.widthClass {
topInset = 0.0
} else if let currentTopInset = self.topInset {
topInset = self.isExpanded ? 0.0 : currentTopInset
} else {
topInset = listSize.height - 46.0 - floor(56.0 * 3.5)
}
var bottomEdge: CGFloat = 0.0
if case .regular = layout.metrics.widthClass {
bottomEdge = size.height
} else {
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListViewItemNode {
let convertedFrame = self.listNode.view.convert(itemNode.frame, to: self.contentContainer.view)
if convertedFrame.maxY > bottomEdge {
bottomEdge = convertedFrame.maxY
}
}
}
if bottomEdge.isZero {
bottomEdge = self.listNode.frame.minY + 46.0 + 56.0
}
}
let rawPanelOffset = topInset + listTopInset - topPanelHeight
let panelOffset = max(layoutTopInset, rawPanelOffset)
let topPanelFrame: CGRect
if isLandscape {
topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: topPanelHeight))
} else {
topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOffset), size: CGSize(width: size.width, height: topPanelHeight))
}
let sideInset: CGFloat = 14.0
let bottomPanelCoverHeight = bottomAreaHeight + layout.intrinsicInsets.bottom
var bottomGradientFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomPanelCoverHeight), size: CGSize(width: size.width, height: bottomGradientHeight))
if isLandscape {
bottomGradientFrame.origin.y = layout.size.height
}
let transitionContainerFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
transition.updateFrame(node: self.transitionContainerNode, frame: transitionContainerFrame)
transition.updateFrame(view: self.transitionMaskView, frame: CGRect(x: 0.0, y: 0.0, width: transitionContainerFrame.width, height: transitionContainerFrame.height))
let updateMaskLayers = {
var topPanelFrame = topPanelFrame
if self.animatingContextMenu {
topPanelFrame.origin.y = 0.0
}
transition.updateFrame(layer: self.transitionMaskTopFillLayer, frame: CGRect(x: 0.0, y: 0.0, width: transitionContainerFrame.width, height: topPanelFrame.maxY))
transition.updateFrame(layer: self.transitionMaskFillLayer, frame: CGRect(x: 0.0, y: topPanelFrame.maxY, width: transitionContainerFrame.width, height: bottomGradientFrame.minY - topPanelFrame.maxY))
transition.updateFrame(layer: self.transitionMaskGradientLayer, frame: CGRect(x: 0.0, y: bottomGradientFrame.minY, width: transitionContainerFrame.width, height: bottomGradientFrame.height))
transition.updateFrame(layer: self.transitionMaskBottomFillLayer, frame: CGRect(x: 0.0, y: bottomGradientFrame.minY, width: transitionContainerFrame.width, height: max(0.0, transitionContainerFrame.height - bottomGradientFrame.minY)))
}
if transition.isAnimated {
updateMaskLayers()
} else {
CATransaction.begin()
CATransaction.setDisableActions(true)
updateMaskLayers()
CATransaction.commit()
}
var bottomInset: CGFloat = 0.0
if case .compact = layout.metrics.widthClass, case let .fullscreen(controlsHidden) = self.displayMode {
if !controlsHidden {
bottomInset = 80.0
}
}
transition.updateAlpha(node: self.bottomGradientNode, alpha: self.isLandscape ? 0.0 : 1.0)
var isTablet = false
let videoFrame: CGRect
let videoContainerFrame: CGRect
if case .regular = layout.metrics.widthClass {
isTablet = true
let videoTopEdgeY = topPanelFrame.maxY
let videoBottomEdgeY = layout.size.height - layout.intrinsicInsets.bottom
videoFrame = CGRect(x: sideInset, y: 0.0, width: contentLeftInset - sideInset, height: videoBottomEdgeY - videoTopEdgeY)
videoContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: videoTopEdgeY), size: CGSize(width: contentLeftInset, height: layout.size.height))
} else {
let videoTopEdgeY = isLandscape ? 0.0 : layoutTopInset
let videoBottomEdgeY = self.isLandscape ? layout.size.height : layout.size.height - layout.intrinsicInsets.bottom - 92.0
videoFrame = CGRect(x: 0.0, y: videoTopEdgeY, width: isLandscape ? max(0.0, layout.size.width - layout.safeInsets.right - 92.0) : layout.size.width, height: videoBottomEdgeY - videoTopEdgeY)
videoContainerFrame = CGRect(origin: CGPoint(), size: layout.size)
}
transition.updateFrame(node: self.mainStageContainerNode, frame: videoContainerFrame)
transition.updateFrame(node: self.mainStageBackgroundNode, frame: videoFrame)
if !self.mainStageNode.animating {
transition.updateFrame(node: self.mainStageNode, frame: videoFrame)
}
self.mainStageNode.update(size: videoFrame.size, sideInset: layout.safeInsets.left, bottomInset: self.isLandscape ? 0.0 : bottomInset, isLandscape: videoFrame.width > videoFrame.height, isTablet: isTablet, transition: transition)
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: size.width, height: layout.size.height))
let leftBorderFrame: CGRect
let rightBorderFrame: CGRect
let additionalInset: CGFloat = 60.0
let additionalSideInset = (size.width - contentWidth) / 2.0
let additionalLeftInset = size.width / 2.0
if isLandscape {
leftBorderFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY - additionalInset), size: CGSize(width: (size.width - contentWidth) / 2.0 + sideInset, height: layout.size.height))
rightBorderFrame = CGRect(origin: CGPoint(x: size.width - (size.width - contentWidth) / 2.0 - sideInset, y: topPanelFrame.maxY - additionalInset), size: CGSize(width: layout.safeInsets.right + (size.width - contentWidth) / 2.0 + sideInset, height: layout.size.height))
} else {
var isFullscreen = false
if case .fullscreen = self.displayMode {
isFullscreen = true
forceUpdate = true
}
leftBorderFrame = CGRect(origin: CGPoint(x: -additionalInset - additionalLeftInset, y: topPanelFrame.maxY - additionalInset * (isFullscreen ? 0.95 : 0.8)), size: CGSize(width: sideInset + additionalInset + additionalLeftInset + (contentLeftInset.isZero ? additionalSideInset : contentLeftInset), height: layout.size.height))
rightBorderFrame = CGRect(origin: CGPoint(x: size.width - sideInset - (contentLeftInset.isZero ? additionalSideInset : 0.0), y: topPanelFrame.maxY - additionalInset * (isFullscreen ? 0.95 : 0.8)), size: CGSize(width: sideInset + additionalInset + additionalLeftInset + additionalSideInset, height: layout.size.height))
}
let topCornersFrame = CGRect(x: sideInset + (contentLeftInset.isZero ? floorToScreenPixels((size.width - contentWidth) / 2.0) : contentLeftInset), y: topPanelFrame.maxY - 60.0, width: contentWidth - sideInset * 2.0, height: 50.0 + 60.0)
let previousTopPanelFrame = self.topPanelNode.frame
let previousBackgroundFrame = self.backgroundNode.frame
let previousLeftBorderFrame = self.leftBorderNode.frame
let previousRightBorderFrame = self.rightBorderNode.frame
if !topPanelFrame.equalTo(previousTopPanelFrame) || forceUpdate {
if topPanelFrame.width != previousTopPanelFrame.width {
transition.updateFrame(node: self.topPanelNode, frame: topPanelFrame)
transition.updateFrame(node: self.topCornersNode, frame: topCornersFrame)
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.leftBorderNode, frame: leftBorderFrame)
transition.updateFrame(node: self.rightBorderNode, frame: rightBorderFrame)
} else {
self.topPanelNode.frame = topPanelFrame
let positionDelta = CGPoint(x: 0.0, y: topPanelFrame.minY - previousTopPanelFrame.minY)
transition.animateOffsetAdditive(layer: self.topPanelNode.layer, offset: positionDelta.y, completion: completion)
transition.updateFrame(node: self.topCornersNode, frame: topCornersFrame)
self.backgroundNode.frame = backgroundFrame
let backgroundPositionDelta = CGPoint(x: 0.0, y: previousBackgroundFrame.minY - backgroundFrame.minY)
transition.animatePositionAdditive(node: self.backgroundNode, offset: backgroundPositionDelta)
self.leftBorderNode.frame = leftBorderFrame
let leftBorderPositionDelta = CGPoint(x: previousLeftBorderFrame.maxX - leftBorderFrame.maxX, y: previousLeftBorderFrame.minY - leftBorderFrame.minY)
transition.animatePositionAdditive(node: self.leftBorderNode, offset: leftBorderPositionDelta)
self.rightBorderNode.frame = rightBorderFrame
let rightBorderPositionDelta = CGPoint(x: previousRightBorderFrame.minX - rightBorderFrame.minX, y: previousRightBorderFrame.minY - rightBorderFrame.minY)
transition.animatePositionAdditive(node: self.rightBorderNode, offset: rightBorderPositionDelta)
}
} else {
completion?()
}
self.topPanelBackgroundNode.frame = CGRect(x: 0.0, y: topPanelHeight - 24.0, width: size.width, height: min(topPanelFrame.height, 24.0))
let listMaxY = listTopInset + listSize.height
let bottomOffset = min(0.0, bottomEdge - listMaxY) + layout.size.height - bottomPanelHeight
let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset + floorToScreenPixels((size.width - contentWidth) / 2.0), y: -50.0 + bottomOffset + bottomGradientHeight), size: CGSize(width: contentWidth - sideInset * 2.0, height: 50.0 + 60.0))
let bottomPanelBackgroundFrame = CGRect(x: 0.0, y: bottomOffset + bottomGradientHeight, width: size.width, height: 2000.0)
let previousBottomCornersFrame = self.bottomCornersNode.frame
if !bottomCornersFrame.equalTo(previousBottomCornersFrame) {
if bottomCornersFrame.width != previousBottomCornersFrame.width {
transition.updateFrame(node: self.bottomCornersNode, frame: bottomCornersFrame)
transition.updateFrame(node: self.bottomPanelBackgroundNode, frame: bottomPanelBackgroundFrame)
} else {
self.bottomCornersNode.frame = bottomCornersFrame
self.bottomPanelBackgroundNode.frame = bottomPanelBackgroundFrame
let positionDelta = CGPoint(x: 0.0, y: previousBottomCornersFrame.minY - bottomCornersFrame.minY)
transition.animatePositionAdditive(node: self.bottomCornersNode, offset: positionDelta)
transition.animatePositionAdditive(node: self.bottomPanelBackgroundNode, offset: positionDelta)
}
}
}
private var decorationsAreDark: Bool?
private var ignoreLayout = false
private func updateDecorationsColors() {
guard let (layout, _) = self.validLayout else {
return
}
let isFullscreen = self.isFullscreen
let effectiveDisplayMode = self.displayMode
self.ignoreLayout = true
self.controller?.statusBar.updateStatusBarStyle(isFullscreen ? .White : .Ignore, animated: true)
self.ignoreLayout = false
let size = layout.size
let topEdgeFrame: CGRect
if isFullscreen {
let offset: CGFloat
if let statusBarHeight = layout.statusBarHeight {
offset = statusBarHeight
} else {
offset = 44.0
}
topEdgeFrame = CGRect(x: 0.0, y: -offset, width: size.width, height: topPanelHeight + offset)
} else {
topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)
}
let backgroundColor: UIColor
if case .fullscreen = effectiveDisplayMode {
backgroundColor = isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor
} else if self.isScheduling || self.callState?.scheduleTimestamp != nil {
backgroundColor = panelBackgroundColor
} else {
backgroundColor = isFullscreen ? panelBackgroundColor : secondaryPanelBackgroundColor
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear)
transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame)
transition.updateCornerRadius(node: self.topPanelEdgeNode, cornerRadius: isFullscreen ? layout.deviceMetrics.screenCornerRadius - 0.5 : 12.0)
transition.updateBackgroundColor(node: self.topPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.topPanelEdgeNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.backgroundNode, color: backgroundColor)
transition.updateBackgroundColor(node: self.bottomPanelBackgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.leftBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
transition.updateBackgroundColor(node: self.rightBorderNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
var gridNode: VoiceChatTilesGridItemNode?
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatTilesGridItemNode {
gridNode = itemNode
}
}
if let gridNode = gridNode {
transition.updateBackgroundColor(node: gridNode.backgroundNode, color: isFullscreen ? fullscreenBackgroundColor : panelBackgroundColor)
}
let previousDark = self.decorationsAreDark
self.decorationsAreDark = isFullscreen
if previousDark != self.decorationsAreDark {
if let snapshotView = self.topCornersNode.view.snapshotContentTree() {
snapshotView.frame = self.topCornersNode.bounds
self.topCornersNode.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
self.topCornersNode.image = decorationTopCornersImage(dark: isFullscreen)
if let snapshotView = self.bottomCornersNode.view.snapshotContentTree() {
snapshotView.frame = self.bottomCornersNode.bounds
self.bottomCornersNode.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
self.bottomCornersNode.image = decorationBottomCornersImage(dark: isFullscreen)
if let gridNode = gridNode {
if let snapshotView = gridNode.cornersNode.view.snapshotContentTree() {
snapshotView.frame = gridNode.cornersNode.bounds
gridNode.cornersNode.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
gridNode.cornersNode.image = decorationCornersImage(top: true, bottom: false, dark: isFullscreen)
gridNode.supernode?.addSubnode(gridNode)
}
UIView.transition(with: self.bottomGradientNode.view, duration: 0.3, options: [.transitionCrossDissolve, .curveLinear]) {
self.bottomGradientNode.backgroundColor = decorationBottomGradientImage(dark: isFullscreen).flatMap { UIColor(patternImage: $0) }
} completion: { _ in
}
self.closeButton.setContent(.image(closeButtonImage(dark: isFullscreen)), animated: transition.isAnimated)
self.optionsButton.setContent(.more(optionsCircleImage(dark: isFullscreen)), animated: transition.isAnimated)
self.panelButton.setContent(.image(panelButtonImage(dark: isFullscreen)), animated: transition.isAnimated)
}
self.updateTitle(transition: transition)
}
private func updateTitle(slide: Bool = false, transition: ContainedViewLayoutTransition) {
guard let _ = self.validLayout else {
return
}
var title = self.currentTitle
if self.isScheduling {
if let peer = self.peer as? TelegramChannel, case .broadcast = peer.info {
title = self.presentationData.strings.ScheduleVoiceChatChannel_Title
} else {
title = self.presentationData.strings.ScheduleVoiceChat_Title
}
} else if case .modal(_, false) = self.displayMode, !self.currentTitleIsCustom {
if let navigationController = self.controller?.navigationController as? NavigationController {
for controller in navigationController.viewControllers.reversed() {
if let controller = controller as? ChatController, case let .peer(peerId) = controller.chatLocation, peerId == self.call.peerId {
if let peer = self.peer as? TelegramChannel, case .broadcast = peer.info {
title = self.presentationData.strings.VoiceChatChannel_Title
} else {
title = self.presentationData.strings.VoiceChat_Title
}
}
}
}
}
var subtitle = self.currentSpeakingSubtitle ?? self.currentSubtitle
var speaking = self.currentSpeakingSubtitle != nil
if self.isScheduling {
subtitle = ""
speaking = false
} else if self.callState?.scheduleTimestamp != nil {
if self.callState?.canManageCall ?? false {
subtitle = self.presentationData.strings.VoiceChat_TapToEditTitle
} else {
subtitle = self.presentationData.strings.VoiceChat_Scheduled
}
speaking = false
}
self.titleNode.update(size: CGSize(width: self.titleNode.bounds.width, height: 44.0), title: title, subtitle: subtitle, speaking: speaking, slide: slide, transition: transition)
}
private func updateButtons(transition: ContainedViewLayoutTransition) {
guard let (layout, _) = self.validLayout else {
return
}
var audioMode: CallControllerButtonsSpeakerMode = .none
//var hasAudioRouteMenu: Bool = false
if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput {
//hasAudioRouteMenu = availableOutputs.count > 2
switch currentOutput {
case .builtin:
audioMode = .builtin
case .speaker:
audioMode = .speaker
case .headphones:
audioMode = .headphones
case let .port(port):
var type: CallControllerButtonsSpeakerMode.BluetoothType = .generic
let portName = port.name.lowercased()
if portName.contains("airpods max") {
type = .airpodsMax
} else if portName.contains("airpods pro") {
type = .airpodsPro
} else if portName.contains("airpods") {
type = .airpods
}
audioMode = .bluetooth(type)
}
if availableOutputs.count <= 1 {
audioMode = .none
}
}
let normalButtonAppearance: CallControllerButtonItemNode.Content.Appearance
let activeButtonAppearance: CallControllerButtonItemNode.Content.Appearance
if let color = self.currentNormalButtonColor {
normalButtonAppearance = .color(.custom(color.rgb, 1.0))
} else {
normalButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0))
}
if let color = self.currentActiveButtonColor {
activeButtonAppearance = .color(.custom(color.rgb, 1.0))
} else {
activeButtonAppearance = .color(.custom(self.isFullscreen ? 0x1c1c1e : 0x2c2c2e, 1.0))
}
var soundImage: CallControllerButtonItemNode.Content.Image
var soundAppearance: CallControllerButtonItemNode.Content.Appearance = normalButtonAppearance
var soundTitle: String = self.presentationData.strings.Call_Speaker
switch audioMode {
case .none, .builtin:
soundImage = .speaker
case .speaker:
soundImage = .speaker
soundAppearance = activeButtonAppearance
case .headphones:
soundImage = .headphones
soundTitle = self.presentationData.strings.Call_Audio
case let .bluetooth(type):
switch type {
case .generic:
soundImage = .bluetooth
case .airpods:
soundImage = .airpods
case .airpodsPro:
soundImage = .airpodsPro
case .airpodsMax:
soundImage = .airpodsMax
}
soundTitle = self.presentationData.strings.Call_Audio
}
let isScheduled = self.isScheduling || self.callState?.scheduleTimestamp != nil
var isSoundEnabled = true
if isScheduled {
if let callState = self.callState, let peer = self.peer, !callState.canManageCall && (peer.addressName?.isEmpty ?? true) {
isSoundEnabled = false
} else {
soundImage = .share
soundTitle = self.presentationData.strings.VoiceChat_ShareShort
soundAppearance = normalButtonAppearance
}
}
let audioButtonSize: CGSize
var buttonsTitleAlpha: CGFloat
let effectiveDisplayMode = self.displayMode
let hasCameraButton = self.cameraButton.isUserInteractionEnabled
let hasVideo = self.call.hasVideo
switch effectiveDisplayMode {
case .modal:
audioButtonSize = hasCameraButton ? smallButtonSize : sideButtonSize
buttonsTitleAlpha = 1.0
case .fullscreen:
if case .regular = layout.metrics.widthClass {
audioButtonSize = hasCameraButton ? smallButtonSize : sideButtonSize
} else {
audioButtonSize = sideButtonSize
}
if case .regular = layout.metrics.widthClass {
buttonsTitleAlpha = 1.0
} else {
buttonsTitleAlpha = 0.0
}
}
self.cameraButton.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: hasVideo ? activeButtonAppearance : normalButtonAppearance, image: hasVideo ? .cameraOn : .cameraOff), text: self.presentationData.strings.VoiceChat_Video, transition: transition)
self.switchCameraButton.update(size: audioButtonSize, content: CallControllerButtonItemNode.Content(appearance: normalButtonAppearance, image: .flipCamera), text: "", transition: transition)
transition.updateAlpha(node: self.switchCameraButton, alpha: hasCameraButton && hasVideo ? 1.0 : 0.0)
transition.updateTransformScale(node: self.switchCameraButton, scale: hasCameraButton && hasVideo ? 1.0 : 0.0)
transition.updateTransformScale(node: self.cameraButton, scale: hasCameraButton ? 1.0 : 0.0)
let hasAudioButton = !self.isScheduling
transition.updateAlpha(node: self.audioButton, alpha: hasCameraButton || !hasAudioButton ? 0.0 : 1.0)
transition.updateTransformScale(node: self.audioButton, scale: hasCameraButton || !hasAudioButton ? 0.0 : 1.0)
self.audioButton.update(size: audioButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage, isEnabled: isSoundEnabled), text: soundTitle, transition: transition)
self.audioButton.isUserInteractionEnabled = isSoundEnabled
self.leaveButton.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.custom(0xff3b30, 0.3)), image: .cancel), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate)
transition.updateAlpha(node: self.cameraButton.textNode, alpha: buttonsTitleAlpha)
transition.updateAlpha(node: self.switchCameraButton.textNode, alpha: buttonsTitleAlpha)
transition.updateAlpha(node: self.audioButton.textNode, alpha: buttonsTitleAlpha)
transition.updateAlpha(node: self.leaveButton.textNode, alpha: buttonsTitleAlpha)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
guard !self.ignoreLayout else {
return
}
let isFirstTime = self.validLayout == nil
let previousLayout = self.validLayout?.0
self.validLayout = (layout, navigationHeight)
let size = layout.size
let contentWidth: CGFloat
let headerWidth: CGFloat
let contentLeftInset: CGFloat
if case .regular = layout.metrics.widthClass {
contentWidth = max(320.0, min(375.0, floor(size.width * 0.3)))
headerWidth = size.width
if self.peerIdToEndpointId.isEmpty {
contentLeftInset = 0.0
} else {
contentLeftInset = self.panelHidden ? layout.size.width : layout.size.width - contentWidth
}
} else {
contentWidth = self.isLandscape ? min(530.0, size.width - 210.0) : size.width
headerWidth = contentWidth
contentLeftInset = 0.0
}
var previousIsLandscape = false
if let previousLayout = previousLayout, case .compact = previousLayout.metrics.widthClass, previousLayout.size.width > previousLayout.size.height {
previousIsLandscape = true
}
var shouldSwitchToExpanded = false
if case let .modal(isExpanded, _) = self.displayMode {
if previousIsLandscape != self.isLandscape && !isExpanded {
shouldSwitchToExpanded = true
} else if case .regular = layout.metrics.widthClass, !isExpanded {
shouldSwitchToExpanded = true
}
}
if shouldSwitchToExpanded {
self.displayMode = .modal(isExpanded: true, isFilled: true)
self.updateDecorationsColors()
self.updateDecorationsLayout(transition: transition)
self.updateMembers()
} else if case .fullscreen = self.displayMode, previousIsLandscape != self.isLandscape {
self.updateMembers()
}
let effectiveDisplayMode = self.displayMode
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - headerWidth) / 2.0), y: 10.0), size: CGSize(width: headerWidth, height: 44.0)))
self.updateTitle(transition: transition)
transition.updateFrame(node: self.optionsButton, frame: CGRect(origin: CGPoint(x: 20.0 + floorToScreenPixels((size.width - headerWidth) / 2.0), y: 18.0), size: CGSize(width: 28.0, height: 28.0)))
transition.updateFrame(node: self.panelButton, frame: CGRect(origin: CGPoint(x: size.width - floorToScreenPixels((size.width - headerWidth) / 2.0) - 20.0 - 28.0 - 38.0 - 24.0, y: 18.0), size: CGSize(width: 38.0, height: 28.0)))
transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - floorToScreenPixels((size.width - headerWidth) / 2.0) - 20.0 - 28.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0)))
transition.updateAlpha(node: self.optionsButton, alpha: self.optionsButton.isUserInteractionEnabled ? 1.0 : 0.0)
transition.updateAlpha(node: self.panelButton, alpha: self.panelButton.isUserInteractionEnabled ? 1.0 : 0.0)
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.contentContainer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: 0.0), size: size))
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
let sideInset: CGFloat = 14.0
var listInsets = UIEdgeInsets()
listInsets.left = sideInset + (self.isLandscape ? 0.0 : layout.safeInsets.left)
listInsets.right = sideInset + (self.isLandscape ? 0.0 : layout.safeInsets.right)
let topEdgeOffset: CGFloat
if let statusBarHeight = layout.statusBarHeight {
topEdgeOffset = statusBarHeight
} else {
topEdgeOffset = 44.0
}
if self.isLandscape {
transition.updateFrame(node: self.topPanelEdgeNode, frame: CGRect(x: 0.0, y: -topEdgeOffset, width: size.width, height: topPanelHeight + topEdgeOffset))
} else if let _ = self.panGestureArguments {
} else {
let topEdgeFrame: CGRect
if self.isFullscreen {
topEdgeFrame = CGRect(x: 0.0, y: -topEdgeOffset, width: size.width, height: topPanelHeight + topEdgeOffset)
} else {
topEdgeFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)
}
transition.updateFrame(node: self.topPanelEdgeNode, frame: topEdgeFrame)
}
let bottomPanelHeight = self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom
var listTopInset = layoutTopInset + topPanelHeight
if self.isLandscape {
listTopInset = topPanelHeight
}
let listSize = CGSize(width: contentWidth, height: layout.size.height - listTopInset - (self.isLandscape ? layout.intrinsicInsets.bottom : bottomPanelHeight) + bottomGradientHeight)
let topInset: CGFloat
if let (panInitialTopInset, panOffset) = self.panGestureArguments {
if self.isExpanded {
topInset = min(self.topInset ?? listSize.height, panInitialTopInset + max(0.0, panOffset))
} else {
topInset = max(0.0, panInitialTopInset + min(0.0, panOffset))
}
} else if case .regular = layout.metrics.widthClass {
topInset = 0.0
} else if let currentTopInset = self.topInset {
topInset = self.isExpanded ? 0.0 : currentTopInset
} else {
topInset = listSize.height - 46.0 - floor(56.0 * 3.5) - bottomGradientHeight
}
transition.updateFrameAsPositionAndBounds(node: self.listContainer, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: contentLeftInset.isZero ? floorToScreenPixels((size.width - contentWidth) / 2.0) : contentLeftInset, y: listTopInset + topInset), size: listSize))
let tileGridSize = CGSize(width: max(0.0, contentLeftInset - sideInset), height: size.height - layout.intrinsicInsets.bottom - listTopInset - topInset)
if contentLeftInset > 0.0 {
self.tileGridNode.isHidden = false
}
if !self.tileGridNode.isHidden {
let _ = self.tileGridNode.update(size: tileGridSize, layoutMode: .grid, items: self.currentTileItems, transition: transition, completion: { [weak self] in
if contentLeftInset.isZero && transition.isAnimated {
self?.tileGridNode.isHidden = true
}
})
}
transition.updateFrame(node: self.tileGridNode, frame: CGRect(origin: CGPoint(x: sideInset, y: listTopInset + topInset), size: tileGridSize))
self.tileGridNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: tileGridSize), within: tileGridSize)
listInsets.bottom = bottomGradientHeight
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: listSize, insets: listInsets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
let fullscreenListWidth: CGFloat
let fullscreenListHeight: CGFloat = 84.0
let fullscreenListTransform: CATransform3D
let fullscreenListInset: CGFloat = 14.0
let fullscreenListUpdateSizeAndInsets: ListViewUpdateSizeAndInsets
let fullscreenListContainerFrame: CGRect
if self.isLandscape {
fullscreenListWidth = layout.size.height
fullscreenListTransform = CATransform3DIdentity
fullscreenListUpdateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: fullscreenListHeight, height: layout.size.height), insets: UIEdgeInsets(top: fullscreenListInset, left: 0.0, bottom: fullscreenListInset, right: 0.0), duration: duration, curve: curve)
fullscreenListContainerFrame = CGRect(x: layout.size.width - min(self.effectiveBottomAreaHeight, fullscreenBottomAreaHeight) - layout.safeInsets.right - fullscreenListHeight - 4.0, y: 0.0, width: fullscreenListHeight, height: layout.size.height)
} else {
fullscreenListWidth = layout.size.width
fullscreenListTransform = CATransform3DMakeRotation(-CGFloat(CGFloat.pi / 2.0), 0.0, 0.0, 1.0)
fullscreenListUpdateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: fullscreenListHeight, height: layout.size.width), insets: UIEdgeInsets(top: fullscreenListInset + layout.safeInsets.left, left: 0.0, bottom: fullscreenListInset + layout.safeInsets.left, right: 0.0), duration: duration, curve: curve)
fullscreenListContainerFrame = CGRect(x: 0.0, y: layout.size.height - min(bottomPanelHeight, fullscreenBottomAreaHeight + layout.intrinsicInsets.bottom) - fullscreenListHeight - 4.0, width: layout.size.width, height: fullscreenListHeight)
}
transition.updateFrame(node: self.fullscreenListContainer, frame: fullscreenListContainerFrame)
self.fullscreenListNode.bounds = CGRect(x: 0.0, y: 0.0, width: fullscreenListHeight, height: fullscreenListWidth)
transition.updatePosition(node: self.fullscreenListNode, position: CGPoint(x: fullscreenListContainerFrame.width / 2.0, y: fullscreenListContainerFrame.height / 2.0))
self.fullscreenListNode.transform = fullscreenListTransform
self.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: fullscreenListUpdateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if case .regular = layout.metrics.widthClass {
self.transitionContainerNode.view.mask = nil
} else {
self.transitionContainerNode.view.mask = self.transitionMaskView
}
var childrenLayout = layout
var childrenInsets = childrenLayout.intrinsicInsets
var childrenSafeInsets = childrenLayout.safeInsets
if case .regular = layout.metrics.widthClass {
let childrenLayoutWidth: CGFloat = 375.0
if contentLeftInset.isZero {
childrenSafeInsets.left = floorToScreenPixels((size.width - childrenLayoutWidth) / 2.0)
childrenSafeInsets.right = floorToScreenPixels((size.width - childrenLayoutWidth) / 2.0)
} else {
childrenSafeInsets.left = floorToScreenPixels((contentLeftInset - childrenLayoutWidth) / 2.0)
childrenSafeInsets.right = childrenSafeInsets.left + (size.width - contentLeftInset)
}
} else if !self.isLandscape, case .fullscreen = effectiveDisplayMode {
childrenInsets.bottom += self.effectiveBottomAreaHeight + fullscreenListHeight + 36.0
}
childrenLayout.safeInsets = childrenSafeInsets
childrenLayout.intrinsicInsets = childrenInsets
self.controller?.presentationContext.containerLayoutUpdated(childrenLayout, transition: transition)
var bottomPanelLeftInset = contentLeftInset
var bottomPanelWidth = size.width - contentLeftInset
if case .regular = layout.metrics.widthClass, bottomPanelLeftInset.isZero {
bottomPanelLeftInset = floorToScreenPixels((layout.size.width - contentWidth) / 2.0)
bottomPanelWidth = contentWidth
}
var bottomPanelFrame = CGRect(origin: CGPoint(x: bottomPanelLeftInset, y: layout.size.height - bottomPanelHeight), size: CGSize(width: bottomPanelWidth, height: bottomPanelHeight))
let bottomPanelCoverHeight = bottomAreaHeight + layout.intrinsicInsets.bottom
if self.isLandscape {
bottomPanelFrame = CGRect(origin: CGPoint(x: layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right, y: 0.0), size: CGSize(width: fullscreenBottomAreaHeight + layout.safeInsets.right, height: layout.size.height))
}
let bottomGradientFrame = CGRect(origin: CGPoint(x: bottomPanelLeftInset, y: layout.size.height - bottomPanelCoverHeight), size: CGSize(width: bottomPanelWidth, height: bottomGradientHeight))
transition.updateFrame(node: self.bottomGradientNode, frame: bottomGradientFrame)
transition.updateFrame(node: self.bottomPanelNode, frame: bottomPanelFrame)
if let pickerView = self.pickerView {
transition.updateFrame(view: pickerView, frame: CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0))
}
let timerFrame = CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0)
transition.updateFrame(node: self.timerNode, frame: timerFrame)
self.timerNode.update(size: timerFrame.size, scheduleTime: self.callState?.scheduleTimestamp, transition: .immediate)
let scheduleTextSize = self.scheduleTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
self.scheduleTextNode.frame = CGRect(origin: CGPoint(x: floor((size.width - scheduleTextSize.width) / 2.0), y: layout.size.height - layout.intrinsicInsets.bottom - scheduleTextSize.height - 145.0), size: scheduleTextSize)
let centralButtonSide = min(contentWidth, size.height) - 32.0
let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide)
let cameraButtonSize = smallButtonSize
let sideButtonMinimalInset: CGFloat = 16.0
let sideButtonOffset = min(42.0, floor((((contentWidth - 112.0) / 2.0) - sideButtonSize.width) / 2.0))
let sideButtonOrigin = max(sideButtonMinimalInset, floor((contentWidth - 112.0) / 2.0) - sideButtonOffset - sideButtonSize.width)
let smallButtons: Bool
if case .regular = layout.metrics.widthClass {
smallButtons = false
} else {
switch effectiveDisplayMode {
case .modal:
smallButtons = self.isLandscape
case .fullscreen:
smallButtons = true
}
}
let actionButtonState: VoiceChatActionButton.State
let actionButtonTitle: String
let actionButtonSubtitle: String
var actionButtonEnabled = true
if let callState = self.callState, !self.isScheduling {
if callState.scheduleTimestamp != nil {
self.ignoreConnecting = true
if callState.canManageCall {
actionButtonState = .scheduled(state: .start)
actionButtonTitle = self.presentationData.strings.VoiceChat_StartNow
actionButtonSubtitle = ""
} else {
if callState.subscribedToScheduled {
actionButtonState = .scheduled(state: .unsubscribe)
actionButtonTitle = self.presentationData.strings.VoiceChat_CancelReminder
} else {
actionButtonState = .scheduled(state: .subscribe)
actionButtonTitle = self.presentationData.strings.VoiceChat_SetReminder
}
actionButtonSubtitle = ""
}
} else {
let connected = self.ignoreConnecting || callState.networkState == .connected
if case .connected = callState.networkState {
self.ignoreConnecting = false
self.ignoreConnectingTimer?.invalidate()
self.ignoreConnectingTimer = nil
} else if self.ignoreConnecting {
if self.ignoreConnectingTimer == nil {
let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
if let strongSelf = self {
strongSelf.ignoreConnecting = false
strongSelf.ignoreConnectingTimer?.invalidate()
strongSelf.ignoreConnectingTimer = nil
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
}
}
}, queue: Queue.mainQueue())
self.ignoreConnectingTimer = timer
timer.start()
}
}
if connected {
if let muteState = callState.muteState, !self.pushingToTalk && !self.temporaryPushingToTalk {
if muteState.canUnmute {
actionButtonState = .active(state: .muted)
actionButtonTitle = self.presentationData.strings.VoiceChat_Unmute
actionButtonSubtitle = ""
} else {
actionButtonState = .active(state: .cantSpeak)
if callState.raisedHand {
actionButtonTitle = self.presentationData.strings.VoiceChat_AskedToSpeak
actionButtonSubtitle = self.presentationData.strings.VoiceChat_AskedToSpeakHelp
} else {
actionButtonTitle = self.presentationData.strings.VoiceChat_MutedByAdmin
actionButtonSubtitle = self.presentationData.strings.VoiceChat_MutedByAdminHelp
}
}
} else {
actionButtonState = .active(state: .on)
actionButtonTitle = self.pushingToTalk ? self.presentationData.strings.VoiceChat_Live : self.presentationData.strings.VoiceChat_Mute
actionButtonSubtitle = ""
}
} else {
actionButtonState = .connecting
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
actionButtonSubtitle = ""
actionButtonEnabled = false
}
}
} else {
if self.isScheduling {
actionButtonState = .button(text: self.scheduleButtonTitle)
actionButtonTitle = ""
actionButtonSubtitle = ""
actionButtonEnabled = true
} else {
actionButtonState = .connecting
actionButtonTitle = self.presentationData.strings.VoiceChat_Connecting
actionButtonSubtitle = ""
actionButtonEnabled = false
}
}
self.actionButton.isDisabled = !actionButtonEnabled
self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, dark: self.isFullscreen, small: smallButtons, animated: true)
let isVideoEnabled = self.callState?.isVideoEnabled ?? false
var hasCameraButton = isVideoEnabled
if let joinedVideo = self.joinedVideo {
hasCameraButton = joinedVideo
}
if !isVideoEnabled {
hasCameraButton = false
}
switch actionButtonState {
case let .active(state):
switch state {
case .cantSpeak:
hasCameraButton = false
case .on, .muted:
break
}
case .connecting:
if !self.connectedOnce {
hasCameraButton = false
}
case .scheduled, .button:
hasCameraButton = false
}
let hasVideo = hasCameraButton && self.call.hasVideo
let upperButtonDistance: CGFloat = 12.0
let firstButtonFrame: CGRect
let secondButtonFrame: CGRect
let thirdButtonFrame: CGRect
let forthButtonFrame: CGRect
let leftButtonFrame: CGRect
if self.isScheduled || !hasVideo {
leftButtonFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize)
} else {
leftButtonFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height - upperButtonDistance - cameraButtonSize.height) / 2.0) + upperButtonDistance + cameraButtonSize.height), size: sideButtonSize)
}
let rightButtonFrame = CGRect(origin: CGPoint(x: contentWidth - sideButtonOrigin - sideButtonSize.width, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize)
var centerButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize)
if case .regular = layout.metrics.widthClass {
centerButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentWidth - centralButtonSize.width) / 2.0), y: floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize)
if hasCameraButton {
firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize)
} else {
firstButtonFrame = CGRect(origin: CGPoint(x: leftButtonFrame.center.x - cameraButtonSize.width / 2.0, y: leftButtonFrame.center.y - cameraButtonSize.height / 2.0), size: cameraButtonSize)
}
secondButtonFrame = leftButtonFrame
thirdButtonFrame = centerButtonFrame
forthButtonFrame = rightButtonFrame
} else {
switch effectiveDisplayMode {
case .modal:
if self.isLandscape {
let sideInset: CGFloat
let buttonsCount: Int
if hasVideo {
sideInset = 26.0
buttonsCount = 4
} else {
sideInset = 42.0
buttonsCount = 3
}
let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1)))
let x = floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0)
forthButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset), size: sideButtonSize)
let thirdButtonPreFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize)
thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize)
secondButtonFrame = CGRect(origin: CGPoint(x: x, y: thirdButtonPreFrame.maxY + spacing), size: sideButtonSize)
if hasCameraButton {
firstButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height), size: sideButtonSize)
} else {
firstButtonFrame = secondButtonFrame
}
} else {
if hasCameraButton {
firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize)
} else {
firstButtonFrame = CGRect(origin: CGPoint(x: leftButtonFrame.center.x - cameraButtonSize.width / 2.0, y: leftButtonFrame.center.y - cameraButtonSize.height / 2.0), size: cameraButtonSize)
}
secondButtonFrame = leftButtonFrame
thirdButtonFrame = centerButtonFrame
forthButtonFrame = rightButtonFrame
}
case let .fullscreen(controlsHidden):
if self.isLandscape {
let sideInset: CGFloat
let buttonsCount: Int
if hasVideo {
sideInset = 26.0
buttonsCount = 4
} else {
sideInset = 42.0
buttonsCount = 3
}
let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1)))
let x = controlsHidden ? fullscreenBottomAreaHeight + layout.safeInsets.right + 30.0 : floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0)
forthButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset), size: sideButtonSize)
let thirdButtonPreFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize)
thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize)
secondButtonFrame = CGRect(origin: CGPoint(x: x, y: thirdButtonPreFrame.maxY + spacing), size: sideButtonSize)
if hasVideo {
firstButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height), size: sideButtonSize)
} else {
firstButtonFrame = secondButtonFrame
}
} else {
let sideInset: CGFloat
let buttonsCount: Int
if hasVideo {
sideInset = 26.0
buttonsCount = 4
} else {
sideInset = 42.0
buttonsCount = 3
}
let spacing = floor((layout.size.width - sideInset * 2.0 - sideButtonSize.width * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1)))
let y = controlsHidden ? self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom + 30.0: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)
if hasVideo {
firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize)
secondButtonFrame = CGRect(origin: CGPoint(x: firstButtonFrame.maxX + spacing, y: y), size: sideButtonSize)
} else {
firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize)
secondButtonFrame = firstButtonFrame
}
let thirdButtonPreFrame = CGRect(origin: CGPoint(x: secondButtonFrame.maxX + spacing, y: y), size: sideButtonSize)
thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize)
forthButtonFrame = CGRect(origin: CGPoint(x: thirdButtonPreFrame.maxX + spacing, y: y), size: sideButtonSize)
}
}
}
let buttonHeight = self.scheduleCancelButton.updateLayout(width: size.width - 32.0, transition: .immediate)
self.scheduleCancelButton.frame = CGRect(x: 16.0, y: 137.0, width: size.width - 32.0, height: buttonHeight)
if self.actionButton.supernode === self.bottomPanelNode {
transition.updateFrame(node: self.actionButton, frame: thirdButtonFrame, completion: transition.isAnimated ? { [weak self] _ in
self?.animatingExpansion = false
} : nil)
}
self.cameraButton.isUserInteractionEnabled = hasCameraButton
var buttonsTransition: ContainedViewLayoutTransition = .immediate
if !isFirstTime {
if case .animated(_, .spring) = transition {
buttonsTransition = transition
} else {
buttonsTransition = .animated(duration: 0.3, curve: .linear)
}
}
self.updateButtons(transition: buttonsTransition)
if self.audioButton.supernode === self.bottomPanelNode {
transition.updateAlpha(node: self.cameraButton, alpha: hasCameraButton ? 1.0 : 0.0)
transition.updateFrameAsPositionAndBounds(node: self.switchCameraButton, frame: firstButtonFrame)
if !self.animatingButtonsSwap || transition.isAnimated {
transition.updateFrameAsPositionAndBounds(node: self.audioButton, frame: secondButtonFrame, completion: { [weak self] _ in
self?.animatingButtonsSwap = false
})
transition.updateFrameAsPositionAndBounds(node: self.cameraButton, frame: secondButtonFrame)
}
transition.updateFrameAsPositionAndBounds(node: self.leaveButton, frame: forthButtonFrame)
}
if isFirstTime {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
while !self.enqueuedFullscreenTransitions.isEmpty {
self.dequeueFullscreenTransition()
}
}
}
private var appIsActive = true {
didSet {
if self.appIsActive != oldValue {
self.updateVisibility()
self.updateRequestedVideoChannels()
}
}
}
private var visibility = false {
didSet {
if self.visibility != oldValue {
self.updateVisibility()
self.updateRequestedVideoChannels()
}
}
}
private func updateVisibility() {
let visible = self.appIsActive && self.visibility
if self.tileGridNode.isHidden {
self.tileGridNode.visibility = false
} else {
self.tileGridNode.visibility = visible
}
self.mainStageNode.visibility = visible
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatTilesGridItemNode {
itemNode.gridVisibility = visible
}
}
self.fullscreenListNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode {
itemNode.gridVisibility = visible
}
}
self.videoRenderingContext.updateVisibility(isVisible: visible)
}
func animateIn() {
guard let (layout, navigationHeight) = self.validLayout else {
return
}
self.visibility = true
self.updateDecorationsLayout(transition: .immediate)
self.animatingAppearance = true
let initialBounds = self.contentContainer.bounds
let topPanelFrame = self.topPanelNode.view.convert(self.topPanelNode.bounds, to: self.view)
self.contentContainer.bounds = initialBounds.offsetBy(dx: 0.0, dy: -(layout.size.height - topPanelFrame.minY))
self.contentContainer.isHidden = false
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
transition.animateView({
self.contentContainer.view.bounds = initialBounds
}, completion: { _ in
self.animatingAppearance = false
if self.actionButton.supernode !== self.bottomPanelNode {
self.actionButton.ignoreHierarchyChanges = true
self.audioButton.isHidden = false
self.cameraButton.isHidden = false
self.leaveButton.isHidden = false
self.audioButton.layer.removeAllAnimations()
self.cameraButton.layer.removeAllAnimations()
self.leaveButton.layer.removeAllAnimations()
self.bottomPanelNode.addSubnode(self.cameraButton)
self.bottomPanelNode.addSubnode(self.audioButton)
self.bottomPanelNode.addSubnode(self.leaveButton)
self.bottomPanelNode.addSubnode(self.actionButton)
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
self.actionButton.ignoreHierarchyChanges = false
}
self.controller?.currentOverlayController?.dismiss()
self.controller?.currentOverlayController = nil
})
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut(completion: (() -> Void)?) {
guard let (layout, _) = self.validLayout else {
return
}
var offsetCompleted = false
let internalCompletion: () -> Void = { [weak self] in
if offsetCompleted {
if let strongSelf = self {
strongSelf.contentContainer.layer.removeAllAnimations()
strongSelf.dimNode.layer.removeAllAnimations()
var bounds = strongSelf.contentContainer.bounds
bounds.origin.y = 0.0
strongSelf.contentContainer.bounds = bounds
strongSelf.visibility = false
}
completion?()
}
}
let topPanelFrame = self.topPanelNode.view.convert(self.topPanelNode.bounds, to: self.view)
self.contentContainer.layer.animateBoundsOriginYAdditive(from: self.contentContainer.bounds.origin.y, to: -(layout.size.height - topPanelFrame.minY) - 44.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
offsetCompleted = true
internalCompletion()
})
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
private func enqueueTransition(_ transition: ListTransition) {
self.enqueuedTransitions.append(transition)
if let _ = self.validLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func enqueueFullscreenTransition(_ transition: ListTransition) {
self.enqueuedFullscreenTransitions.append(transition)
if let _ = self.validLayout {
while !self.enqueuedFullscreenTransitions.isEmpty {
self.dequeueFullscreenTransition()
}
}
}
private func dequeueTransition() {
guard let (layout, _) = self.validLayout, let transition = self.enqueuedTransitions.first else {
return
}
self.enqueuedTransitions.remove(at: 0)
if let callState = self.callState {
if callState.scheduleTimestamp != nil && self.listNode.alpha > 0.0 {
self.timerNode.isHidden = false
self.cameraButton.alpha = 0.0
self.cameraButton.isUserInteractionEnabled = false
self.listNode.alpha = 0.0
self.listNode.isUserInteractionEnabled = false
self.backgroundNode.backgroundColor = panelBackgroundColor
self.updateDecorationsColors()
} else if callState.scheduleTimestamp == nil && !self.isScheduling && self.listNode.alpha == 0.0 {
self.transitionToCall()
}
}
var options = ListViewDeleteAndInsertOptions()
let isFirstTime = self.isFirstTime
if isFirstTime {
self.isFirstTime = false
} else {
if transition.crossFade {
options.insert(.AnimateCrossfade)
}
if transition.animated {
options.insert(.AnimateInsertion)
}
}
options.insert(.LowLatency)
options.insert(.PreferSynchronousResourceLoading)
var size = layout.size
if case .regular = layout.metrics.widthClass {
size.width = floor(min(size.width, size.height) * 0.5)
}
let bottomPanelHeight = self.isLandscape ? layout.intrinsicInsets.bottom : bottomAreaHeight + layout.intrinsicInsets.bottom
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
let listTopInset = layoutTopInset + topPanelHeight
let listSize = CGSize(width: size.width, height: layout.size.height - listTopInset - bottomPanelHeight + bottomGradientHeight)
self.topInset = listSize.height - 46.0 - floor(56.0 * 3.5) - bottomGradientHeight
if transition.animated {
self.animatingInsertion = true
}
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
if isFirstTime {
strongSelf.updateDecorationsLayout(transition: .immediate)
} else if strongSelf.animatingInsertion {
strongSelf.updateDecorationsLayout(transition: .animated(duration: 0.2, curve: .easeInOut))
}
strongSelf.animatingInsertion = false
if !strongSelf.didSetContentsReady {
strongSelf.didSetContentsReady = true
strongSelf.controller?.contentsReady.set(true)
}
strongSelf.updateVisibility()
})
}
private func dequeueFullscreenTransition() {
guard let _ = self.validLayout, let transition = self.enqueuedFullscreenTransitions.first else {
return
}
self.enqueuedFullscreenTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
let isFirstTime = self.isFirstTime
if !isFirstTime {
if transition.animated {
options.insert(.AnimateInsertion)
}
}
self.fullscreenListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
private func updateMembers(maybeUpdateVideo: Bool = true, force: Bool = false) {
self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set(), maybeUpdateVideo: maybeUpdateVideo, force: force)
}
private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: ([GroupCallParticipantsContext.Participant], String?), invitedPeers: [Peer], speakingPeers: Set<PeerId>, maybeUpdateVideo: Bool = true, force: Bool = false) {
var disableAnimation = false
if self.currentCallMembers?.1 != callMembers.1 {
disableAnimation = true
}
let speakingPeersUpdated = self.currentSpeakingPeers != speakingPeers
self.currentCallMembers = callMembers
self.currentInvitedPeers = invitedPeers
var entries: [ListEntry] = []
var fullscreenEntries: [ListEntry] = []
var index: Int32 = 0
var fullscreenIndex: Int32 = 0
var processedPeerIds = Set<PeerId>()
var processedFullscreenPeerIds = Set<PeerId>()
var peerIdToCameraEndpointId: [PeerId: String] = [:]
var peerIdToEndpointId: [PeerId: String] = [:]
var requestedVideoChannels: [PresentationGroupCallRequestedVideo] = []
var gridTileItems: [VoiceChatTileItem] = []
var tileItems: [VoiceChatTileItem] = []
var gridTileByVideoEndpoint: [String: VoiceChatTileItem] = [:]
var tileByVideoEndpoint: [String: VoiceChatTileItem] = [:]
var entryByPeerId: [PeerId: VoiceChatPeerEntry] = [:]
var latestWideVideo: String? = nil
var isTablet = false
var displayPanelVideos = false
if let (layout, _) = self.validLayout, case .regular = layout.metrics.widthClass {
isTablet = true
displayPanelVideos = self.displayPanelVideos
}
var joinedVideo = self.joinedVideo ?? true
var myEntry: VoiceChatPeerEntry?
var mainEntry: VoiceChatPeerEntry?
for member in callMembers.0 {
if processedPeerIds.contains(member.peer.id) {
continue
}
processedPeerIds.insert(member.peer.id)
let memberState: VoiceChatPeerEntry.State
var memberMuteState: GroupCallParticipantsContext.Participant.MuteState?
if member.hasRaiseHand && !(member.muteState?.canUnmute ?? false) {
memberState = .raisedHand
memberMuteState = member.muteState
if self.raisedHandDisplayDisposables[member.peer.id] == nil {
var displayedRaisedHands = self.displayedRaisedHands
displayedRaisedHands.insert(member.peer.id)
self.displayedRaisedHands = displayedRaisedHands
let signal: Signal<Never, NoError> = Signal.complete() |> delay(3.0, queue: Queue.mainQueue())
self.raisedHandDisplayDisposables[member.peer.id] = signal.start(completed: { [weak self] in
if let strongSelf = self {
var displayedRaisedHands = strongSelf.displayedRaisedHands
displayedRaisedHands.remove(member.peer.id)
strongSelf.displayedRaisedHands = displayedRaisedHands
strongSelf.updateMembers()
}
})
}
} else {
if member.peer.id == self.callState?.myPeerId {
if muteState == nil {
memberState = speakingPeers.contains(member.peer.id) ? .speaking : .listening
} else {
memberState = .listening
memberMuteState = member.muteState
}
} else {
memberState = speakingPeers.contains(member.peer.id) ? .speaking : .listening
memberMuteState = member.muteState
}
if let disposable = self.raisedHandDisplayDisposables[member.peer.id] {
disposable.dispose()
self.raisedHandDisplayDisposables[member.peer.id] = nil
}
}
var memberPeer = member.peer
if member.peer.id == self.callState?.myPeerId {
joinedVideo = member.joinedVideo
if let user = memberPeer as? TelegramUser, let photo = self.currentUpdatingAvatar {
memberPeer = user.withUpdatedPhoto([photo])
}
}
joinedVideo = true
if let videoEndpointId = member.videoEndpointId {
peerIdToCameraEndpointId[member.peer.id] = videoEndpointId
}
if let anyEndpointId = member.presentationEndpointId ?? member.videoEndpointId {
peerIdToEndpointId[member.peer.id] = anyEndpointId
}
let peerEntry = VoiceChatPeerEntry(
peer: memberPeer,
about: member.about,
isMyPeer: self.callState?.myPeerId == member.peer.id,
videoEndpointId: member.videoEndpointId,
videoPaused: member.videoDescription?.isPaused ?? false,
presentationEndpointId: member.presentationEndpointId,
presentationPaused: member.presentationDescription?.isPaused ?? false,
effectiveSpeakerVideoEndpointId: self.effectiveSpeaker?.1,
state: memberState,
muteState: memberMuteState,
canManageCall: self.callState?.canManageCall ?? false,
volume: member.volume,
raisedHand: member.hasRaiseHand,
displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id),
active: memberPeer.id == self.effectiveSpeaker?.0,
isLandscape: self.isLandscape
)
if peerEntry.isMyPeer {
myEntry = peerEntry
}
if peerEntry.active {
mainEntry = peerEntry
}
entryByPeerId[peerEntry.peer.id] = peerEntry
var isTile = false
if let interaction = self.itemInteraction {
if let videoEndpointId = member.presentationEndpointId {
if !self.videoOrder.contains(videoEndpointId) {
if peerEntry.isMyPeer {
self.videoOrder.insert(videoEndpointId, at: 0)
} else {
self.videoOrder.append(videoEndpointId)
}
}
if isTablet {
if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.presentationDescription?.isPaused ?? false, showAsPresentation: peerIdToCameraEndpointId[peerEntry.peer.id] != nil, secondary: false) {
isTile = true
gridTileByVideoEndpoint[videoEndpointId] = tileItem
}
}
if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.presentationDescription?.isPaused ?? false, showAsPresentation: peerIdToCameraEndpointId[peerEntry.peer.id] != nil, secondary: displayPanelVideos) {
isTile = true
tileByVideoEndpoint[videoEndpointId] = tileItem
}
if self.wideVideoNodes.contains(videoEndpointId) {
latestWideVideo = videoEndpointId
}
}
if let videoEndpointId = member.videoEndpointId {
if !self.videoOrder.contains(videoEndpointId) {
if peerEntry.isMyPeer {
self.videoOrder.insert(videoEndpointId, at: 0)
} else {
self.videoOrder.append(videoEndpointId)
}
}
if isTablet {
if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.videoDescription?.isPaused ?? false, showAsPresentation: false, secondary: false) {
isTile = true
gridTileByVideoEndpoint[videoEndpointId] = tileItem
}
}
if let tileItem = ListEntry.peer(peerEntry, 0).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, isTablet: isTablet, videoEndpointId: videoEndpointId, videoReady: self.readyVideoEndpointIds.contains(videoEndpointId), videoTimeouted: self.timeoutedEndpointIds.contains(videoEndpointId), videoIsPaused: member.videoDescription?.isPaused ?? false, showAsPresentation: false, secondary: displayPanelVideos) {
isTile = true
tileByVideoEndpoint[videoEndpointId] = tileItem
}
if self.wideVideoNodes.contains(videoEndpointId) {
latestWideVideo = videoEndpointId
}
}
}
if !isTile || isTablet || !joinedVideo {
entries.append(.peer(peerEntry, index))
}
index += 1
if self.callState?.networkState == .connecting {
} else {
if var videoChannel = member.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .medium) {
if self.effectiveSpeaker?.1 == videoChannel.endpointId {
videoChannel.maxQuality = .full
}
requestedVideoChannels.append(videoChannel)
}
if member.peer.id != self.callState?.myPeerId {
if var presentationChannel = member.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .thumbnail) {
if self.effectiveSpeaker?.1 == presentationChannel.endpointId {
presentationChannel.minQuality = .full
presentationChannel.maxQuality = .full
}
requestedVideoChannels.append(presentationChannel)
}
}
}
}
var temporaryList: [String] = []
for tileVideoEndpoint in self.videoOrder {
if let _ = tileByVideoEndpoint[tileVideoEndpoint] {
temporaryList.append(tileVideoEndpoint)
}
}
if (tileByVideoEndpoint.count % 2) != 0, let last = temporaryList.last, !self.wideVideoNodes.contains(last), let latestWide = latestWideVideo {
self.videoOrder.removeAll(where: { $0 == latestWide })
self.videoOrder.append(latestWide)
}
for tileVideoEndpoint in self.videoOrder {
if let tileItem = gridTileByVideoEndpoint[tileVideoEndpoint] {
gridTileItems.append(tileItem)
}
if let tileItem = tileByVideoEndpoint[tileVideoEndpoint] {
if displayPanelVideos && tileItem.peer.id == self.effectiveSpeaker?.0 {
} else {
tileItems.append(tileItem)
}
if let fullscreenEntry = entryByPeerId[tileItem.peer.id] {
if processedFullscreenPeerIds.contains(tileItem.peer.id) {
continue
}
fullscreenEntries.append(.peer(fullscreenEntry, fullscreenIndex))
processedFullscreenPeerIds.insert(fullscreenEntry.peer.id)
fullscreenIndex += 1
}
}
}
self.joinedVideo = joinedVideo
let configuration = self.configuration ?? VoiceChatConfiguration.defaultValue
var reachedLimit = false
if !joinedVideo && (!tileItems.isEmpty || !gridTileItems.isEmpty), let peer = self.peer {
tileItems.removeAll()
gridTileItems.removeAll()
tileItems.append(VoiceChatTileItem(account: self.context.account, peer: peer, videoEndpointId: "", videoReady: false, videoTimeouted: true, isVideoLimit: true, videoLimit: configuration.videoParticipantsMaxCount, isPaused: false, isOwnScreencast: false, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, speaking: false, secondary: false, isTablet: false, icon: .none, text: .none, additionalText: nil, action: {}, contextAction: nil, getVideo: { _ in return nil }, getAudioLevel: nil))
} else if let callState = self.callState, !tileItems.isEmpty && callState.isVideoWatchersLimitReached && self.connectedOnce && (callState.canManageCall || callState.adminIds.contains(self.context.account.peerId)) {
reachedLimit = true
}
for member in callMembers.0 {
if processedFullscreenPeerIds.contains(member.peer.id) {
continue
}
processedFullscreenPeerIds.insert(member.peer.id)
if let peerEntry = entryByPeerId[member.peer.id] {
fullscreenEntries.append(.peer(peerEntry, fullscreenIndex))
fullscreenIndex += 1
}
}
for peer in invitedPeers {
if processedPeerIds.contains(peer.id) {
continue
}
processedPeerIds.insert(peer.id)
entries.append(.peer(VoiceChatPeerEntry(
peer: peer,
about: nil,
isMyPeer: false,
videoEndpointId: nil,
videoPaused: false,
presentationEndpointId: nil,
presentationPaused: false,
effectiveSpeakerVideoEndpointId: nil,
state: .invited,
muteState: nil,
canManageCall: false,
volume: nil,
raisedHand: false,
displayRaisedHandStatus: false,
active: false,
isLandscape: false
), index))
index += 1
}
self.requestedVideoChannels = requestedVideoChannels
var myVideoUpdated = false
if let previousMyEntry = self.myEntry, let myEntry = myEntry, previousMyEntry.effectiveVideoEndpointId == nil && myEntry.effectiveVideoEndpointId != nil && self.currentForcedSpeaker == nil {
self.currentDominantSpeaker = (myEntry.peer.id, myEntry.effectiveVideoEndpointId, CACurrentMediaTime())
myVideoUpdated = true
}
self.myEntry = myEntry
guard self.didSetDataReady && (force || (!self.isPanning && !self.animatingExpansion && !self.animatingMainStage)) else {
return
}
let previousMainEntry = self.mainEntry
self.mainEntry = mainEntry
if let mainEntry = mainEntry {
self.mainStageNode.update(peerEntry: mainEntry, pinned: self.currentForcedSpeaker != nil)
if let previousMainEntry = previousMainEntry, maybeUpdateVideo {
if previousMainEntry.effectiveVideoEndpointId != mainEntry.effectiveVideoEndpointId || previousMainEntry.videoPaused != mainEntry.videoPaused || myVideoUpdated {
self.updateMainVideo(waitForFullSize: true, entries: fullscreenEntries, force: true)
return
}
}
} else if self.effectiveSpeaker != nil, !fullscreenEntries.isEmpty {
self.updateMainVideo(waitForFullSize: true, entries: fullscreenEntries, force: true)
return
}
self.updateRequestedVideoChannels()
self.currentSpeakingPeers = speakingPeers
self.peerIdToEndpointId = peerIdToEndpointId
var updateLayout = false
var animatingLayout = false
if self.currentTileItems.isEmpty != gridTileItems.isEmpty {
animatingLayout = true
updateLayout = true
}
if isTablet {
updateLayout = true
self.currentTileItems = gridTileItems
if displayPanelVideos && !tileItems.isEmpty {
entries.insert(.tiles(tileItems, .pairs, configuration.videoParticipantsMaxCount, reachedLimit), at: 0)
}
} else {
if !tileItems.isEmpty {
entries.insert(.tiles(tileItems, .pairs, configuration.videoParticipantsMaxCount, reachedLimit), at: 0)
}
}
var canInvite = true
var inviteIsLink = false
if let peer = self.peer as? TelegramChannel {
if peer.flags.contains(.isGigagroup) {
if peer.flags.contains(.isCreator) || peer.adminRights != nil {
} else {
canInvite = false
}
}
if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) {
inviteIsLink = true
}
}
if canInvite {
entries.append(.invite(self.presentationData.theme, self.presentationData.strings, inviteIsLink ? self.presentationData.strings.VoiceChat_Share : self.presentationData.strings.VoiceChat_InviteMember, inviteIsLink))
}
let previousEntries = self.currentEntries
let previousFullscreenEntries = self.currentFullscreenEntries
self.currentEntries = entries
self.currentFullscreenEntries = fullscreenEntries
if previousEntries.count == entries.count {
var allEqual = true
for i in 0 ..< previousEntries.count {
if previousEntries[i].stableId != entries[i].stableId {
if case let .peer(lhsPeer, _) = previousEntries[i], case let .peer(rhsPeer, _) = entries[i] {
if lhsPeer.isMyPeer != rhsPeer.isMyPeer {
allEqual = false
break
}
} else {
allEqual = false
break
}
}
}
if allEqual {
disableAnimation = true
}
} else if abs(previousEntries.count - entries.count) > 10 {
disableAnimation = true
}
let presentationData = self.presentationData.withUpdated(theme: self.darkTheme)
let transition = self.preparedTransition(from: previousEntries, to: entries, isLoading: false, isEmpty: false, canInvite: canInvite, crossFade: false, animated: !disableAnimation, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!)
self.enqueueTransition(transition)
let fullscreenTransition = self.preparedFullscreenTransition(from: previousFullscreenEntries, to: fullscreenEntries, isLoading: false, isEmpty: false, canInvite: canInvite, crossFade: false, animated: true, context: self.context, presentationData: presentationData, interaction: self.itemInteraction!)
if !isTablet {
self.enqueueFullscreenTransition(fullscreenTransition)
}
if speakingPeersUpdated {
var speakingPeers = speakingPeers
var updatedSpeakers: [PeerId] = []
for peerId in self.currentSpeakers {
if speakingPeers.contains(peerId) {
updatedSpeakers.append(peerId)
speakingPeers.remove(peerId)
}
}
var currentSpeakingSubtitle = ""
for peerId in Array(speakingPeers) {
updatedSpeakers.append(peerId)
if let peer = entryByPeerId[peerId]?.peer {
let displayName = speakingPeers.count == 1 ? peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) : peer.compactDisplayTitle
if currentSpeakingSubtitle.isEmpty {
currentSpeakingSubtitle.append(displayName)
} else {
currentSpeakingSubtitle.append(", \(displayName)")
}
}
}
self.currentSpeakers = updatedSpeakers
self.currentSpeakingSubtitle = currentSpeakingSubtitle.isEmpty ? nil : currentSpeakingSubtitle
self.updateTitle(transition: .immediate)
}
if case .fullscreen = self.displayMode, !self.mainStageNode.animating {
if speakingPeersUpdated {
self.mainStageNode.update(speakingPeerId: self.currentSpeakers.first)
}
} else {
self.mainStageNode.update(speakingPeerId: nil)
}
if updateLayout, let (layout, navigationHeight) = self.validLayout {
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .easeInOut)
if animatingLayout {
self.animatingExpansion = true
}
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
self.updateDecorationsLayout(transition: transition)
}
}
private func callStateDidReset() {
self.requestedVideoSources.removeAll()
self.filterRequestedVideoChannels(channels: [])
self.updateRequestedVideoChannels()
}
private func filterRequestedVideoChannels(channels: [PresentationGroupCallRequestedVideo]) {
var validSources = Set<String>()
for channel in channels {
validSources.insert(channel.endpointId)
if !self.requestedVideoSources.contains(channel.endpointId) {
self.requestedVideoSources.insert(channel.endpointId)
let input = (self.call as! PresentationGroupCallImpl).video(endpointId: channel.endpointId)
if let input = input, let videoView = self.videoRenderingContext.makeView(input: input, blur: false) {
let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: self.videoRenderingContext.makeView(input: input, blur: true))
self.readyVideoDisposables.set((combineLatest(videoNode.ready, .single(false) |> then(.single(true) |> delay(10.0, queue: Queue.mainQueue())))
|> deliverOnMainQueue
).start(next: { [weak self, weak videoNode] ready, timeouted in
if let strongSelf = self, let videoNode = videoNode {
Queue.mainQueue().after(0.1) {
if timeouted && !ready {
strongSelf.timeoutedEndpointIds.insert(channel.endpointId)
strongSelf.readyVideoEndpointIds.remove(channel.endpointId)
strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds)
strongSelf.wideVideoNodes.remove(channel.endpointId)
strongSelf.updateMembers()
} else if ready {
strongSelf.readyVideoEndpointIds.insert(channel.endpointId)
strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds)
strongSelf.timeoutedEndpointIds.remove(channel.endpointId)
if videoNode.aspectRatio <= 0.77 {
strongSelf.wideVideoNodes.insert(channel.endpointId)
} else {
strongSelf.wideVideoNodes.remove(channel.endpointId)
}
strongSelf.updateMembers()
if let (layout, _) = strongSelf.validLayout, case .compact = layout.metrics.widthClass {
if let interaction = strongSelf.itemInteraction {
loop: for i in 0 ..< strongSelf.currentFullscreenEntries.count {
let entry = strongSelf.currentFullscreenEntries[i]
switch entry {
case let .peer(peerEntry, _):
if peerEntry.effectiveVideoEndpointId == channel.endpointId {
let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme)
strongSelf.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.fullscreenItem(context: strongSelf.context, presentationData: presentationData, interaction: interaction), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
break loop
}
default:
break
}
}
}
}
}
}
}
}), forKey: channel.endpointId)
self.videoNodes[channel.endpointId] = videoNode
if let _ = self.validLayout {
self.updateMembers()
}
}
/*self.call.makeIncomingVideoView(endpointId: channel.endpointId, requestClone: GroupVideoNode.useBlurTransparency, completion: { [weak self] videoView, backdropVideoView in
Queue.mainQueue().async {
guard let strongSelf = self, let videoView = videoView else {
return
}
let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: backdropVideoView)
strongSelf.readyVideoDisposables.set((combineLatest(videoNode.ready, .single(false) |> then(.single(true) |> delay(10.0, queue: Queue.mainQueue())))
|> deliverOnMainQueue
).start(next: { [weak self, weak videoNode] ready, timeouted in
if let strongSelf = self, let videoNode = videoNode {
Queue.mainQueue().after(0.1) {
if timeouted && !ready {
strongSelf.timeoutedEndpointIds.insert(channel.endpointId)
strongSelf.readyVideoEndpointIds.remove(channel.endpointId)
strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds)
strongSelf.wideVideoNodes.remove(channel.endpointId)
strongSelf.updateMembers()
} else if ready {
strongSelf.readyVideoEndpointIds.insert(channel.endpointId)
strongSelf.readyVideoEndpointIdsPromise.set(strongSelf.readyVideoEndpointIds)
strongSelf.timeoutedEndpointIds.remove(channel.endpointId)
if videoNode.aspectRatio <= 0.77 {
strongSelf.wideVideoNodes.insert(channel.endpointId)
} else {
strongSelf.wideVideoNodes.remove(channel.endpointId)
}
strongSelf.updateMembers()
if let (layout, _) = strongSelf.validLayout, case .compact = layout.metrics.widthClass {
if let interaction = strongSelf.itemInteraction {
loop: for i in 0 ..< strongSelf.currentFullscreenEntries.count {
let entry = strongSelf.currentFullscreenEntries[i]
switch entry {
case let .peer(peerEntry, _):
if peerEntry.effectiveVideoEndpointId == channel.endpointId {
let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme)
strongSelf.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.fullscreenItem(context: strongSelf.context, presentationData: presentationData, interaction: interaction), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
break loop
}
default:
break
}
}
}
}
}
}
}
}), forKey: channel.endpointId)
strongSelf.videoNodes[channel.endpointId] = videoNode
if let _ = strongSelf.validLayout {
strongSelf.updateMembers()
}
}
})*/
}
}
var removeRequestedVideoSources: [String] = []
for source in self.requestedVideoSources {
if !validSources.contains(source) {
removeRequestedVideoSources.append(source)
}
}
for source in removeRequestedVideoSources {
self.requestedVideoSources.remove(source)
}
for (videoEndpointId, _) in self.videoNodes {
if !validSources.contains(videoEndpointId) {
self.videoNodes[videoEndpointId] = nil
self.videoOrder.removeAll(where: { $0 == videoEndpointId })
self.readyVideoEndpointIds.remove(videoEndpointId)
self.readyVideoEndpointIdsPromise.set(self.readyVideoEndpointIds)
self.readyVideoDisposables.set(nil, forKey: videoEndpointId)
}
}
}
private func updateMainVideo(waitForFullSize: Bool, entries: [ListEntry]? = nil, updateMembers: Bool = true, force: Bool = false, completion: (() -> Void)? = nil) {
let effectiveMainSpeaker = self.currentForcedSpeaker ?? self.currentDominantSpeaker.flatMap { ($0.0, $0.1) }
guard effectiveMainSpeaker?.0 != self.effectiveSpeaker?.0 || effectiveMainSpeaker?.1 != self.effectiveSpeaker?.1 || force else {
return
}
let currentEntries = entries ?? self.currentFullscreenEntries
var effectiveSpeaker: (PeerId, String?, Bool, Bool, Bool)? = nil
var anySpeakerWithVideo: (PeerId, String?, Bool, Bool, Bool)? = nil
var anySpeaker: (PeerId, Bool)? = nil
if let (peerId, preferredVideoEndpointId) = effectiveMainSpeaker {
for entry in currentEntries {
switch entry {
case let .peer(peer, _):
if peer.peer.id == peerId {
if let preferredVideoEndpointId = preferredVideoEndpointId, peer.videoEndpointId == preferredVideoEndpointId || peer.presentationEndpointId == preferredVideoEndpointId {
var isPaused = false
if peer.presentationEndpointId != nil && preferredVideoEndpointId == peer.presentationEndpointId {
isPaused = peer.presentationPaused
} else if peer.videoEndpointId != nil && preferredVideoEndpointId == peer.videoEndpointId {
isPaused = peer.videoPaused
}
effectiveSpeaker = (peerId, preferredVideoEndpointId, peer.isMyPeer, peer.presentationEndpointId != nil && preferredVideoEndpointId == peer.presentationEndpointId, isPaused)
} else {
var isPaused = false
if peer.effectiveVideoEndpointId != nil && peer.effectiveVideoEndpointId == peer.presentationEndpointId {
isPaused = peer.presentationPaused
} else if peer.effectiveVideoEndpointId != nil && peer.effectiveVideoEndpointId == peer.videoEndpointId {
isPaused = peer.videoPaused
}
effectiveSpeaker = (peerId, peer.effectiveVideoEndpointId, peer.isMyPeer, peer.presentationEndpointId != nil && peer.effectiveVideoEndpointId == peer.presentationEndpointId, isPaused)
}
} else if anySpeakerWithVideo == nil, let videoEndpointId = peer.effectiveVideoEndpointId {
var isPaused = false
if videoEndpointId == peer.presentationEndpointId {
isPaused = peer.presentationPaused
} else if videoEndpointId == peer.videoEndpointId {
isPaused = peer.videoPaused
}
anySpeakerWithVideo = (peer.peer.id, videoEndpointId, peer.isMyPeer, peer.presentationEndpointId != nil && videoEndpointId == peer.presentationEndpointId, isPaused)
} else if anySpeaker == nil {
anySpeaker = (peer.peer.id, peer.isMyPeer)
}
default:
break
}
}
}
if effectiveSpeaker == nil {
self.currentForcedSpeaker = nil
effectiveSpeaker = anySpeakerWithVideo ?? anySpeaker.flatMap { ($0.0, nil, $0.1, false, false) }
if let (peerId, videoEndpointId, _, _, _) = effectiveSpeaker {
self.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime())
} else {
self.currentDominantSpeaker = nil
}
}
self.effectiveSpeaker = effectiveSpeaker
if updateMembers {
self.updateMembers(maybeUpdateVideo: false, force: force)
}
var waitForFullSize = waitForFullSize
var isReady = false
if let (_, maybeVideoEndpointId, _, _, _) = effectiveSpeaker, let videoEndpointId = maybeVideoEndpointId {
isReady = true
if !self.readyVideoEndpointIds.contains(videoEndpointId) {
isReady = false
if entries == nil {
waitForFullSize = false
}
}
}
self.mainStageNode.update(peer: effectiveSpeaker, isReady: isReady, waitForFullSize: waitForFullSize, completion: {
completion?()
})
}
private func updateRequestedVideoChannels() {
Queue.mainQueue().after(0.3) {
let enableVideo = self.appIsActive && self.visibility
self.call.setRequestedVideoList(items: enableVideo ? self.requestedVideoChannels : [])
self.filterRequestedVideoChannels(channels: self.requestedVideoChannels)
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UILongPressGestureRecognizer {
return !self.isScheduling
} else if gestureRecognizer is DirectionalPanGestureRecognizer {
if self.mainStageNode.animating || self.animatingMainStage {
return false
}
let bottomPanelLocation = gestureRecognizer.location(in: self.bottomPanelNode.view)
let containerLocation = gestureRecognizer.location(in: self.contentContainer.view)
let mainStageLocation = gestureRecognizer.location(in: self.mainStageNode.view)
if self.isLandscape && self.mainStageContainerNode.isUserInteractionEnabled && mainStageLocation.x > self.mainStageNode.frame.width - 80.0 {
return false
}
if self.audioButton.frame.contains(bottomPanelLocation) || (!self.cameraButton.isHidden && self.cameraButton.frame.contains(bottomPanelLocation)) || self.leaveButton.frame.contains(bottomPanelLocation) || self.pickerView?.frame.contains(containerLocation) == true || (self.mainStageContainerNode.isUserInteractionEnabled && (mainStageLocation.y < 44.0 || mainStageLocation.y > self.mainStageNode.frame.height - 100.0)) {
return false
}
}
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
return true
}
return false
}
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
guard let (layout, _) = self.validLayout else {
return
}
let contentOffset = self.listNode.visibleContentOffset()
switch recognizer.state {
case .began:
let topInset: CGFloat
if case .regular = layout.metrics.widthClass {
topInset = 0.0
} else if self.isExpanded {
topInset = 0.0
} else if let currentTopInset = self.topInset {
topInset = currentTopInset
} else {
topInset = self.listNode.frame.height
}
self.panGestureArguments = (topInset, 0.0)
self.controller?.dismissAllTooltips()
if case .fullscreen = self.displayMode, case .compact = layout.metrics.widthClass {
self.isPanning = true
self.mainStageBackgroundNode.alpha = 0.0
self.mainStageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4)
self.mainStageNode.setControlsHidden(true, animated: true)
self.fullscreenListNode.alpha = 0.0
self.fullscreenListNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { [weak self] finished in
self?.attachTileVideos()
self?.fullscreenListContainer.subnodeTransform = CATransform3DIdentity
})
self.listContainer.transform = CATransform3DMakeScale(0.86, 0.86, 1.0)
self.contentContainer.insertSubnode(self.mainStageContainerNode, aboveSubnode: self.bottomPanelNode)
}
case .changed:
var translation = recognizer.translation(in: self.contentContainer.view).y
if self.isScheduled && translation < 0.0 {
return
}
let translateBounds: Bool
if case .regular = layout.metrics.widthClass {
translateBounds = true
} else {
switch self.displayMode {
case let .modal(isExpanded, previousIsFilled):
var topInset: CGFloat = 0.0
if let (currentTopInset, currentPanOffset) = self.panGestureArguments {
topInset = currentTopInset
if case let .known(value) = contentOffset, value <= 0.5 {
} else {
translation = currentPanOffset
if self.isExpanded {
recognizer.setTranslation(CGPoint(), in: self.contentContainer.view)
}
}
self.panGestureArguments = (currentTopInset, translation)
}
let currentOffset = topInset + translation
var isFilled = previousIsFilled
if currentOffset < 20.0 {
isFilled = true
} else if currentOffset > 40.0 {
isFilled = false
}
if isFilled != previousIsFilled {
self.displayMode = .modal(isExpanded: isExpanded, isFilled: isFilled)
self.updateDecorationsColors()
}
if self.isExpanded {
} else {
if currentOffset > 0.0 {
self.listNode.scroller.panGestureRecognizer.setTranslation(CGPoint(), in: self.listNode.scroller)
}
}
case .fullscreen:
if abs(translation) > 32.0 {
if self.fullscreenListNode.layer.animationKeys()?.contains("opacity") == true {
self.fullscreenListNode.layer.removeAllAnimations()
}
}
var bounds = self.mainStageContainerNode.bounds
bounds.origin.y = -translation
self.mainStageContainerNode.bounds = bounds
var backgroundFrame = self.mainStageNode.frame
backgroundFrame.origin.y += -translation
self.mainStageBackgroundNode.frame = backgroundFrame
self.fullscreenListContainer.subnodeTransform = CATransform3DMakeTranslation(0.0, translation, 0.0)
}
translateBounds = !self.isExpanded
}
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
self.updateDecorationsLayout(transition: .immediate)
}
if translateBounds {
var bounds = self.contentContainer.bounds
bounds.origin.y = -translation
bounds.origin.y = min(0.0, bounds.origin.y)
self.contentContainer.bounds = bounds
}
case .ended:
let translation = recognizer.translation(in: self.contentContainer.view)
var velocity = recognizer.velocity(in: self.contentContainer.view)
if case let .known(value) = contentOffset, value > 0.0 {
velocity = CGPoint()
} else if case .unknown = contentOffset {
velocity = CGPoint()
}
var bounds = self.contentContainer.bounds
bounds.origin.y = -translation.y
bounds.origin.y = min(0.0, bounds.origin.y)
let offset: CGFloat
if let (inset, panOffset) = self.panGestureArguments {
offset = inset + panOffset
} else {
offset = 0.0
}
let topInset: CGFloat
if let currentTopInset = self.topInset {
topInset = currentTopInset
} else {
topInset = self.listNode.frame.height
}
if case .fullscreen = self.displayMode, case .compact = layout.metrics.widthClass {
self.panGestureArguments = nil
self.fullscreenListContainer.subnodeTransform = CATransform3DIdentity
if abs(translation.y) > 100.0 || abs(velocity.y) > 300.0 {
self.mainStageBackgroundNode.layer.removeAllAnimations()
self.currentForcedSpeaker = nil
self.updateDisplayMode(.modal(isExpanded: true, isFilled: true), fromPan: true)
self.effectiveSpeaker = nil
} else {
self.isPanning = false
self.mainStageBackgroundNode.alpha = 1.0
self.mainStageBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, completion: { [weak self] _ in
self?.attachFullscreenVideos()
})
self.mainStageNode.setControlsHidden(false, animated: true, delay: 0.15)
self.fullscreenListNode.alpha = 1.0
self.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.15)
var bounds = self.mainStageContainerNode.bounds
let previousBounds = bounds
bounds.origin.y = 0.0
self.mainStageContainerNode.bounds = bounds
self.mainStageContainerNode.layer.animateBounds(from: previousBounds, to: self.mainStageContainerNode.bounds, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.listContainer.transform = CATransform3DIdentity
strongSelf.contentContainer.insertSubnode(strongSelf.mainStageContainerNode, belowSubnode: strongSelf.transitionContainerNode)
strongSelf.updateMembers()
}
})
}
} else if case .modal(true, _) = self.displayMode, case .compact = layout.metrics.widthClass {
self.panGestureArguments = nil
if velocity.y > 300.0 || offset > topInset / 2.0 {
self.displayMode = .modal(isExpanded: false, isFilled: false)
self.updateDecorationsColors()
self.animatingExpansion = true
self.listNode.scroller.setContentOffset(CGPoint(), animated: false)
let distance: CGFloat
if let topInset = self.topInset {
distance = topInset - offset
} else {
distance = 0.0
}
let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance)
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
}
self.updateDecorationsLayout(transition: transition, completion: {
self.animatingExpansion = false
})
} else {
self.displayMode = .modal(isExpanded: true, isFilled: true)
self.updateDecorationsColors()
self.animatingExpansion = true
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
}
self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
self.animatingExpansion = false
})
}
} else {
self.panGestureArguments = nil
var dismissing = false
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
if self.isScheduling {
self.dismissScheduled()
} else if case .regular = layout.metrics.widthClass {
self.controller?.dismiss(closing: false, manual: true)
dismissing = true
} else {
if case .fullscreen = self.displayMode {
} else {
self.controller?.dismiss(closing: false, manual: true)
dismissing = true
}
}
} else if !self.isScheduling && (velocity.y < -300.0 || offset < topInset / 2.0) {
if velocity.y > -2200.0 && !self.isFullscreen {
DispatchQueue.main.async {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}
let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset)
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
if case .modal = self.displayMode {
self.displayMode = .modal(isExpanded: true, isFilled: true)
}
self.updateDecorationsColors()
self.animatingExpansion = true
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
}
self.updateDecorationsLayout(transition: transition, completion: {
self.animatingExpansion = false
})
} else if !self.isScheduling {
self.updateDecorationsColors()
self.animatingExpansion = true
self.listNode.scroller.setContentOffset(CGPoint(), animated: false)
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
}
self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
self.animatingExpansion = false
})
}
if !dismissing {
var bounds = self.contentContainer.bounds
let previousBounds = bounds
bounds.origin.y = 0.0
self.contentContainer.bounds = bounds
self.contentContainer.layer.animateBounds(from: previousBounds, to: self.contentContainer.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
}
case .cancelled:
self.panGestureArguments = nil
let previousBounds = self.contentContainer.bounds
var bounds = self.contentContainer.bounds
bounds.origin.y = 0.0
self.contentContainer.bounds = bounds
self.contentContainer.layer.animateBounds(from: previousBounds, to: self.contentContainer.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
}
self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut), completion: {
self.animatingExpansion = false
})
if case .fullscreen = self.displayMode, case .regular = layout.metrics.widthClass {
self.fullscreenListContainer.subnodeTransform = CATransform3DIdentity
self.isPanning = false
self.mainStageBackgroundNode.alpha = 1.0
self.mainStageBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, completion: { [weak self] _ in
self?.attachFullscreenVideos()
})
self.mainStageNode.setControlsHidden(false, animated: true, delay: 0.15)
self.fullscreenListNode.alpha = 1.0
self.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.15)
var bounds = self.mainStageContainerNode.bounds
let previousBounds = bounds
bounds.origin.y = 0.0
self.mainStageContainerNode.bounds = bounds
self.mainStageContainerNode.layer.animateBounds(from: previousBounds, to: self.mainStageContainerNode.bounds, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.contentContainer.insertSubnode(strongSelf.mainStageContainerNode, belowSubnode: strongSelf.transitionContainerNode)
strongSelf.updateMembers()
strongSelf.listContainer.transform = CATransform3DIdentity
}
})
}
default:
break
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self.topPanelNode.view {
return self.view
}
if result === self.bottomPanelNode.view {
return self.view
}
if !self.bounds.contains(point) {
return nil
}
if point.y < self.topPanelNode.frame.minY {
return self.dimNode.view
}
return result
}
fileprivate func scrollToTop() {
if self.isExpanded {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}
private func openTitleEditing() {
let _ = (self.context.account.postbox.loadedPeerWithId(self.call.peerId)
|> deliverOnMainQueue).start(next: { [weak self] chatPeer in
guard let strongSelf = self else {
return
}
let initialTitle = strongSelf.callState?.title ?? ""
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EditTitleTitle, text: strongSelf.presentationData.strings.VoiceChat_EditTitleText, placeholder: chatPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { title in
if let strongSelf = self, let title = title, title != initialTitle {
strongSelf.call.updateTitle(title)
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).string), action: { _ in return false })
}
})
strongSelf.controller?.present(controller, in: .window(.root))
})
}
private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) {
guard let peerId = self.callState?.myPeerId else {
return
}
let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in
return (transaction.getPeer(peerId), currentSearchBotsConfiguration(transaction: transaction))
}
|> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in
guard let strongSelf = self, let peer = peer else {
return
}
let presentationData = strongSelf.presentationData
let legacyController = LegacyController(presentation: .custom, theme: strongSelf.darkTheme)
legacyController.statusBar.statusBarStyle = .Ignore
let emptyController = LegacyEmptyController(context: legacyController.context)!
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0)
legacyController.bind(controller: navigationController)
strongSelf.view.endEditing(true)
strongSelf.controller?.present(legacyController, in: .window(.root))
var hasPhotos = false
if !peer.profileImageRepresentations.isEmpty {
hasPhotos = true
}
let paintStickersContext = LegacyPaintStickersContext(context: strongSelf.context)
// paintStickersContext.presentStickersController = { completion in
// let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
// let coder = PostboxEncoder()
// coder.encodeRootObject(fileReference.media)
// completion?(coder.makeData(), fileReference.media.isAnimatedSticker, node.view, rect)
// return true
// })
// strongSelf.controller?.present(controller, in: .window(.root))
// return controller
// }
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)!
mixin.forceDark = true
mixin.stickersContext = paintStickersContext
let _ = strongSelf.currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { [weak self] assetsController in
guard let strongSelf = self else {
return
}
let controller = WebSearchController(context: strongSelf.context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in
assetsController?.dismiss()
self?.updateProfilePhoto(result)
}))
controller.navigationPresentation = .modal
strongSelf.controller?.push(controller)
if fromGallery {
completion()
}
}
mixin.didFinishWithImage = { [weak self] image in
if let image = image {
completion()
self?.updateProfilePhoto(image)
}
}
mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in
if let image = image, let asset = asset {
completion()
self?.updateProfileVideo(image, asset: asset, adjustments: adjustments)
}
}
mixin.didFinishWithDelete = {
guard let strongSelf = self else {
return
}
let proceed = {
let _ = strongSelf.currentAvatarMixin.swap(nil)
let postbox = strongSelf.context.account.postbox
strongSelf.updateAvatarDisposable.set((strongSelf.context.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
|> deliverOnMainQueue).start())
}
let actionSheet = ActionSheetController(presentationData: presentationData.withUpdated(theme: strongSelf.darkTheme))
let items: [ActionSheetItem] = [
ActionSheetButtonItem(title: presentationData.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
proceed()
})
]
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
}
mixin.didDismiss = { [weak legacyController] in
guard let strongSelf = self else {
return
}
let _ = strongSelf.currentAvatarMixin.swap(nil)
legacyController?.dismiss()
}
let menuController = mixin.present()
if let menuController = menuController {
menuController.customRemoveFromParentViewController = { [weak legacyController] in
legacyController?.dismiss()
}
}
})
}
private func updateProfilePhoto(_ image: UIImage) {
guard let data = image.jpegData(compressionQuality: 0.6), let peerId = self.callState?.myPeerId else {
return
}
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
self.call.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)
self.currentUpdatingAvatar = representation
self.updateAvatarPromise.set(.single((representation, 0.0)))
let postbox = self.call.account.postbox
let signal = peerId.namespace == Namespaces.Peer.CloudUser ? self.call.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
}) : self.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: self.call.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
self.updateAvatarDisposable.set((signal
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
switch result {
case .complete:
strongSelf.updateAvatarPromise.set(.single(nil))
case let .progress(value):
strongSelf.updateAvatarPromise.set(.single((representation, value)))
}
}))
self.updateMembers()
}
private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) {
guard let data = image.jpegData(compressionQuality: 0.6), let peerId = self.callState?.myPeerId else {
return
}
let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
self.context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil)
self.currentUpdatingAvatar = representation
self.updateAvatarPromise.set(.single((representation, 0.0)))
var videoStartTimestamp: Double? = nil
if let adjustments = adjustments, adjustments.videoStartValue > 0.0 {
videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue
}
let context = self.context
let account = self.context.account
let signal = Signal<TelegramMediaResource, UploadPeerPhotoError> { [weak self] subscriber in
let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in
if let paintingData = adjustments.paintingData, paintingData.hasAnimation {
return LegacyPaintEntityRenderer(account: account, adjustments: adjustments)
} else {
return nil
}
}
let uploadInterface = LegacyLiveUploadInterface(context: context)
let signal: SSignal
if let asset = asset as? AVAsset {
signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)!
} else if let url = asset as? URL, let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer {
let durationSignal: SSignal = SSignal(generator: { subscriber in
let disposable = (entityRenderer.duration()).start(next: { duration in
subscriber.putNext(duration)
subscriber.putCompletion()
})
return SBlockDisposable(block: {
disposable.dispose()
})
})
signal = durationSignal.map(toSignal: { duration -> SSignal in
if let duration = duration as? Double {
return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, watcher: nil, entityRenderer: entityRenderer)!
} else {
return SSignal.single(nil)
}
})
} else {
signal = SSignal.complete()
}
let signalDisposable = signal.start(next: { next in
if let result = next as? TGMediaVideoConversionResult {
if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) {
account.postbox.mediaBox.storeResourceData(photoResource.id, data: data)
}
if let timestamp = videoStartTimestamp {
videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05))
}
var value = stat()
if stat(result.fileURL.path, &value) == 0 {
if let data = try? Data(contentsOf: result.fileURL) {
let resource: TelegramMediaResource
if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult {
resource = LocalFileMediaResource(fileId: liveUploadData.id)
} else {
resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
}
account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
subscriber.putNext(resource)
}
}
subscriber.putCompletion()
} else if let strongSelf = self, let progress = next as? NSNumber {
Queue.mainQueue().async {
strongSelf.updateAvatarPromise.set(.single((representation, Float(truncating: progress) * 0.25)))
}
}
}, error: { _ in
}, completed: nil)
let disposable = ActionDisposable {
signalDisposable?.dispose()
}
return ActionDisposable {
disposable.dispose()
}
}
self.updateAvatarDisposable.set((signal
|> mapToSignal { videoResource -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> in
if peerId.namespace == Namespaces.Peer.CloudUser {
return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
} else {
return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
})
}
}
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
switch result {
case .complete:
strongSelf.updateAvatarPromise.set(.single(nil))
case let .progress(value):
strongSelf.updateAvatarPromise.set(.single((representation, 0.25 + value * 0.75)))
}
}))
}
private func displayUnmuteTooltip() {
guard let (layout, _) = self.validLayout else {
return
}
let location = self.actionButton.view.convert(self.actionButton.bounds, to: self.view).center
var point = CGRect(origin: CGPoint(x: location.x - 5.0, y: location.y - 5.0 - 68.0), size: CGSize(width: 10.0, height: 10.0))
var position: TooltipScreen.ArrowPosition = .bottom
if case .compact = layout.metrics.widthClass {
if self.isLandscape {
point.origin.x = location.x - 5.0 - 36.0
point.origin.y = location.y - 5.0
position = .right
} else if case .fullscreen = self.displayMode {
point.origin.y += 32.0
}
}
self.controller?.present(TooltipScreen(text: self.presentationData.strings.VoiceChat_UnmuteSuggestion, style: .gradient(UIColor(rgb: 0x1d446c), UIColor(rgb: 0x193e63)), icon: nil, location: .point(point, position), displayDuration: .custom(8.0), shouldDismissOnTouch: { _ in
return .dismiss(consume: false)
}), in: .window(.root))
}
private var isScheduled: Bool {
return self.isScheduling || self.callState?.scheduleTimestamp != nil
}
private func attachFullscreenVideos() {
guard let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass else {
return
}
var verticalItemNodes: [String: ASDisplayNode] = [:]
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatTilesGridItemNode {
for tileNode in itemNode.tileNodes {
if let item = tileNode.item {
verticalItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] = tileNode
}
if tileNode.item?.peer.id == self.effectiveSpeaker?.0 && tileNode.item?.videoEndpointId == self.effectiveSpeaker?.1 {
tileNode.isHidden = false
}
}
}
}
self.fullscreenListNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item {
let otherItemNode = verticalItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")]
itemNode.transitionIn(from: otherItemNode)
}
}
}
private func attachTileVideos() {
var fullscreenItemNodes: [String: VoiceChatFullscreenParticipantItemNode] = [:]
var tileNodes: [VoiceChatTileItemNode] = []
if !self.tileGridNode.isHidden {
tileNodes = self.tileGridNode.tileNodes
} else {
self.fullscreenListNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item {
fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")] = itemNode
}
}
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatTilesGridItemNode {
tileNodes = itemNode.tileNodes
}
}
}
for tileNode in tileNodes {
if let item = tileNode.item {
let otherItemNode = fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId]
tileNode.transitionIn(from: otherItemNode)
if tileNode.item?.peer.id == self.effectiveSpeaker?.0 && tileNode.item?.videoEndpointId == self.effectiveSpeaker?.1 {
tileNode.isHidden = true
}
}
}
}
private func updateDisplayMode(_ displayMode: DisplayMode, fromPan: Bool = false) {
guard !self.animatingExpansion && !self.animatingMainStage && !self.mainStageNode.animating else {
return
}
self.updateMembers()
let previousDisplayMode = self.displayMode
var isFullscreen = false
if case .fullscreen = displayMode {
isFullscreen = true
}
if case .fullscreen = previousDisplayMode, case .fullscreen = displayMode {
self.animatingExpansion = true
} else {
self.animatingMainStage = true
}
var hasFullscreenList = false
if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass {
hasFullscreenList = true
}
let completion = {
self.displayMode = displayMode
self.updateDecorationsColors()
self.mainStageContainerNode.isHidden = false
self.mainStageContainerNode.isUserInteractionEnabled = isFullscreen
let transition: ContainedViewLayoutTransition = .animated(duration: 0.55, curve: .spring)
if case .modal = previousDisplayMode, case .fullscreen = self.displayMode {
self.mainStageNode.alpha = 0.0
self.updateDecorationsLayout(transition: .immediate)
var verticalItemNodes: [String: ASDisplayNode] = [:]
var tileNodes: [VoiceChatTileItemNode] = []
if !self.tileGridNode.isHidden {
tileNodes = self.tileGridNode.tileNodes
} else {
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatTilesGridItemNode {
tileNodes = itemNode.tileNodes
}
}
}
for tileNode in tileNodes {
if let item = tileNode.item {
verticalItemNodes[String(item.peer.id.toInt64()) + "_" + item.videoEndpointId] = tileNode
}
}
let completion = {
let effectiveSpeakerPeerId = self.effectiveSpeaker?.0
if hasFullscreenList {
self.fullscreenListContainer.isHidden = false
self.fullscreenListNode.alpha = 0.0
}
var gridSnapshotView: UIView?
if !hasFullscreenList, let snapshotView = self.tileGridNode.view.snapshotView(afterScreenUpdates: false) {
gridSnapshotView = snapshotView
self.tileGridNode.view.addSubview(snapshotView)
self.displayPanelVideos = true
self.updateMembers(maybeUpdateVideo: false, force: true)
}
let completion = {
if hasFullscreenList {
self.attachFullscreenVideos()
self.fullscreenListNode.alpha = 1.0
self.fullscreenListNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
if let effectiveSpeakerPeerId = effectiveSpeakerPeerId, let otherItemNode = verticalItemNodes[String(effectiveSpeakerPeerId.toInt64()) + "_" + (self.effectiveSpeaker?.1 ?? "")] {
if hasFullscreenList {
let transitionStartPosition = otherItemNode.view.convert(CGPoint(x: otherItemNode.frame.width / 2.0, y: otherItemNode.frame.height), to: self.fullscreenListContainer.view.superview)
self.fullscreenListContainer.layer.animatePosition(from: transitionStartPosition, to: self.fullscreenListContainer.position, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring)
}
self.mainStageNode.animateTransitionIn(from: otherItemNode, transition: transition, completion: { [weak self] in
self?.animatingMainStage = false
})
self.mainStageNode.alpha = 1.0
self.mainStageBackgroundNode.alpha = 1.0
self.mainStageBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: hasFullscreenList ? 0.13 : 0.3, completion: { [weak otherItemNode] _ in
otherItemNode?.alpha = 0.0
gridSnapshotView?.removeFromSuperview()
completion()
})
} else {
completion()
}
if hasFullscreenList {
self.listContainer.layer.animateScale(from: 1.0, to: 0.86, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring)
}
if self.isLandscape {
self.transitionMaskTopFillLayer.opacity = 1.0
}
self.transitionMaskBottomFillLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
Queue.mainQueue().after(0.3) {
self?.transitionMaskTopFillLayer.opacity = 0.0
self?.transitionMaskBottomFillLayer.removeAllAnimations()
}
})
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
self.updateDecorationsLayout(transition: transition)
}
}
let effectiveSpeakerPeerId = self.effectiveSpeaker?.0
var index = 0
for item in self.currentFullscreenEntries {
if case let .peer(entry, _) = item, entry.peer.id == effectiveSpeakerPeerId {
break
} else {
index += 1
}
}
let position: ListViewScrollPosition
if index > self.currentFullscreenEntries.count - 3 {
index = self.currentFullscreenEntries.count - 1
position = .bottom(0.0)
} else {
position = .center(.bottom)
}
self.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: index, position: position, animated: false, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in
completion()
})
} else if case .fullscreen = previousDisplayMode, case .modal = self.displayMode {
var minimalVisiblePeerid: (PeerId, CGFloat)?
var fullscreenItemNodes: [String: VoiceChatFullscreenParticipantItemNode] = [:]
self.fullscreenListNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item {
let convertedFrame = itemNode.view.convert(itemNode.bounds, to: self.transitionContainerNode.view)
if let (_, x) = minimalVisiblePeerid {
if convertedFrame.minX >= 0.0 && convertedFrame.minX < x {
minimalVisiblePeerid = (item.peer.id, convertedFrame.minX)
}
} else if convertedFrame.minX >= 0.0 {
minimalVisiblePeerid = (item.peer.id, convertedFrame.minX)
}
fullscreenItemNodes[String(item.peer.id.toInt64()) + "_" + (item.videoEndpointId ?? "")] = itemNode
}
}
let completion = {
let effectiveSpeakerPeerId = self.effectiveSpeaker?.0
var targetTileNode: VoiceChatTileItemNode?
self.transitionContainerNode.addSubnode(self.mainStageNode)
self.listContainer.transform = CATransform3DIdentity
var tileNodes: [VoiceChatTileItemNode] = []
if !self.tileGridNode.isHidden {
tileNodes = self.tileGridNode.tileNodes
} else {
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatTilesGridItemNode {
tileNodes = itemNode.tileNodes
}
}
}
for tileNode in tileNodes {
if let item = tileNode.item {
if item.peer.id == effectiveSpeakerPeerId, item.videoEndpointId == self.effectiveSpeaker?.1 {
targetTileNode = tileNode
}
}
}
var transitionOffset = -self.mainStageContainerNode.bounds.minY
if transitionOffset.isZero, let (layout, _) = self.validLayout {
if case .regular = layout.metrics.widthClass {
transitionOffset += 87.0
}
if let targetTileNode = targetTileNode {
let transitionTargetPosition = targetTileNode.view.convert(CGPoint(x: targetTileNode.frame.width / 2.0, y: targetTileNode.frame.height), to: self.fullscreenListContainer.view.superview)
self.fullscreenListContainer.layer.animatePosition(from: self.fullscreenListContainer.position, to: transitionTargetPosition, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring)
}
if !hasFullscreenList {
self.displayPanelVideos = false
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? VoiceChatTilesGridItemNode {
itemNode.snapshotForDismissal()
}
}
self.updateMembers(maybeUpdateVideo: false, force: true)
self.attachTileVideos()
self.mainStageBackgroundNode.alpha = 0.0
self.mainStageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
} else {
self.fullscreenListNode.alpha = 0.0
self.mainStageBackgroundNode.alpha = 1.0
self.fullscreenListNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.fullscreenListContainer.isHidden = true
strongSelf.fullscreenListNode.alpha = 1.0
strongSelf.attachTileVideos()
strongSelf.mainStageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
strongSelf.mainStageBackgroundNode.alpha = 0.0
}
})
}
}
self.mainStageNode.animateTransitionOut(to: targetTileNode, offset: transitionOffset, transition: transition, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.effectiveSpeaker = nil
strongSelf.mainStageNode.update(peer: nil, waitForFullSize: false)
strongSelf.mainStageNode.setControlsHidden(false, animated: false)
strongSelf.fullscreenListContainer.isHidden = true
strongSelf.mainStageContainerNode.isHidden = true
strongSelf.mainStageContainerNode.addSubnode(strongSelf.mainStageNode)
var bounds = strongSelf.mainStageContainerNode.bounds
bounds.origin.y = 0.0
strongSelf.mainStageContainerNode.bounds = bounds
strongSelf.contentContainer.insertSubnode(strongSelf.mainStageContainerNode, belowSubnode: strongSelf.transitionContainerNode)
strongSelf.isPanning = false
strongSelf.animatingMainStage = false
})
if hasFullscreenList {
self.listContainer.layer.animateScale(from: 0.86, to: 1.0, duration: 0.55, timingFunction: kCAMediaTimingFunctionSpring)
}
self.transitionMaskTopFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
if !transitionOffset.isZero {
self.transitionMaskBottomFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
self.updateDecorationsLayout(transition: transition)
}
}
if false, let (peerId, _) = minimalVisiblePeerid {
var index = 0
for item in self.currentEntries {
if case let .peer(entry, _) = item, entry.peer.id == peerId {
break
} else {
index += 1
}
}
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in
completion()
})
} else {
completion()
}
} else if case .fullscreen = self.displayMode {
if let (layout, navigationHeight) = self.validLayout {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
self.updateDecorationsLayout(transition: transition)
}
}
}
if case .fullscreen(false) = displayMode, case .modal = previousDisplayMode {
self.updateMainVideo(waitForFullSize: true, updateMembers: true, force: true, completion: {
completion()
})
} else {
completion()
}
}
fileprivate var actionButtonPosition: CGPoint {
guard let (layout, _) = self.validLayout else {
return CGPoint()
}
let size = layout.size
let hasCameraButton = self.cameraButton.isUserInteractionEnabled
let centralButtonSide = min(size.width, size.height) - 32.0
let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide)
if case .regular = layout.metrics.widthClass {
let contentWidth: CGFloat = max(320.0, min(375.0, floor(size.width * 0.3)))
let contentLeftInset: CGFloat
if self.peerIdToEndpointId.isEmpty {
contentLeftInset = floorToScreenPixels((layout.size.width - contentWidth) / 2.0)
} else {
contentLeftInset = self.panelHidden ? layout.size.width : layout.size.width - contentWidth
}
return CGPoint(x: contentLeftInset + floorToScreenPixels(contentWidth / 2.0), y: layout.size.height - self.effectiveBottomAreaHeight - layout.intrinsicInsets.bottom + floor(self.effectiveBottomAreaHeight / 2.0) - 3.0)
} else {
switch self.displayMode {
case .modal:
if self.isLandscape {
let sideInset: CGFloat
let buttonsCount: Int
if hasCameraButton {
sideInset = 26.0
buttonsCount = 4
} else {
sideInset = 42.0
buttonsCount = 3
}
let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1)))
let x = layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right + floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0)
let actionButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize)
return actionButtonFrame.center
} else {
let actionButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: layout.size.height - self.effectiveBottomAreaHeight - layout.intrinsicInsets.bottom + floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize)
return actionButtonFrame.center
}
case let .fullscreen(controlsHidden):
if self.isLandscape {
let sideInset: CGFloat
let buttonsCount: Int
if hasCameraButton {
sideInset = 26.0
buttonsCount = 4
} else {
sideInset = 42.0
buttonsCount = 3
}
let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1)))
let x = layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right + (controlsHidden ? fullscreenBottomAreaHeight + layout.safeInsets.right + 30.0 : floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0))
let actionButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize)
return actionButtonFrame.center
} else {
let sideInset: CGFloat
let buttonsCount: Int
if hasCameraButton {
sideInset = 26.0
buttonsCount = 4
} else {
sideInset = 42.0
buttonsCount = 3
}
let spacing = floor((layout.size.width - sideInset * 2.0 - sideButtonSize.width * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1)))
let y = layout.size.height - self.effectiveBottomAreaHeight - layout.intrinsicInsets.bottom + (controlsHidden ? self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom + 30.0: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0))
let secondButtonFrame: CGRect
if hasCameraButton {
let firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize)
secondButtonFrame = CGRect(origin: CGPoint(x: firstButtonFrame.maxX + spacing, y: y), size: sideButtonSize)
} else {
secondButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize)
}
let actionButtonFrame = CGRect(origin: CGPoint(x: secondButtonFrame.maxX + spacing, y: y), size: sideButtonSize)
return actionButtonFrame.center
}
}
}
}
}
private let sharedContext: SharedAccountContext
public let call: PresentationGroupCall
private let presentationData: PresentationData
public var parentNavigationController: NavigationController?
fileprivate let contentsReady = ValuePromise<Bool>(false, ignoreRepeated: true)
fileprivate let dataReady = ValuePromise<Bool>(false, ignoreRepeated: true)
fileprivate let audioOutputStateReady = ValuePromise<Bool>(false, ignoreRepeated: true)
private let _ready = Promise<Bool>(false)
override public var ready: Promise<Bool> {
return self._ready
}
public var onViewDidAppear: (() -> Void)?
public var onViewDidDisappear: (() -> Void)?
private var reclaimActionButton: (() -> Void)?
private var didAppearOnce: Bool = false
private var isDismissed: Bool = false
private var isDisconnected: Bool = false
private var controllerNode: Node {
return self.displayNode as! Node
}
private let idleTimerExtensionDisposable = MetaDisposable()
public weak var currentOverlayController: VoiceChatOverlayController?
private var validLayout: ContainerViewLayout?
public init(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) {
self.sharedContext = sharedContext
self.call = call
self.presentationData = sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: nil)
self.automaticallyControlPresentationContextLayout = false
self.blocksBackgroundWhenInOverlay = true
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all)
self.statusBar.statusBarStyle = .Ignore
self._ready.set(combineLatest([
self.contentsReady.get(),
self.dataReady.get(),
self.audioOutputStateReady.get()
])
|> map { values -> Bool in
for value in values {
if !value {
return false
}
}
return true
}
|> filter { $0 })
self.scrollToTop = { [weak self] in
self?.controllerNode.scrollToTop()
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.idleTimerExtensionDisposable.dispose()
if let currentOverlayController = self.currentOverlayController {
currentOverlayController.animateOut(reclaim: false, targetPosition: CGPoint(), completion: { _ in })
}
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self, sharedContext: self.sharedContext, call: self.call)
self.displayNodeDidLoad()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.isDismissed = false
if !self.didAppearOnce {
self.didAppearOnce = true
self.reclaimActionButton?()
self.controllerNode.animateIn()
self.idleTimerExtensionDisposable.set(self.sharedContext.applicationBindings.pushIdleTimerExtension())
}
DispatchQueue.main.async {
self.onViewDidAppear?()
}
}
override public func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.idleTimerExtensionDisposable.set(nil)
DispatchQueue.main.async {
self.didAppearOnce = false
self.isDismissed = true
self.detachActionButton()
self.onViewDidDisappear?()
}
}
private var dismissedManually: Bool = false
public func dismiss(closing: Bool, manual: Bool = false) {
if closing {
self.isDisconnected = true
} else {
if let navigationController = self.navigationController as? NavigationController {
let count = navigationController.viewControllers.count
if count == 2 || navigationController.viewControllers[count - 2] is ChatController {
if case .active(.cantSpeak) = self.controllerNode.actionButton.stateValue {
} else if case .button = self.controllerNode.actionButton.stateValue {
} else if case .scheduled = self.controllerNode.actionButton.stateValue {
} else if let chatController = navigationController.viewControllers[count - 2] as? ChatController, chatController.isSendButtonVisible {
} else if let tabBarController = navigationController.viewControllers[count - 2] as? TabBarController, let chatListController = tabBarController.controllers[tabBarController.selectedIndex] as? ChatListController, chatListController.isSearchActive {
} else {
if manual {
self.dismissedManually = true
Queue.mainQueue().after(0.05) {
self.detachActionButton()
}
} else {
self.detachActionButton()
}
}
}
}
}
self.dismiss()
}
private func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
})
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
}
private func detachActionButton() {
guard self.currentOverlayController == nil && !self.isDisconnected else {
return
}
let overlayController = VoiceChatOverlayController(actionButton: self.controllerNode.actionButton, audioOutputNode: self.controllerNode.audioButton, cameraNode: self.controllerNode.cameraButton, leaveNode: self.controllerNode.leaveButton, navigationController: self.navigationController as? NavigationController, initiallyHidden: self.dismissedManually)
if let navigationController = self.navigationController as? NavigationController {
navigationController.presentOverlay(controller: overlayController, inGlobal: true, blockInteraction: false)
}
self.currentOverlayController = overlayController
self.dismissedManually = false
self.reclaimActionButton = { [weak self, weak overlayController] in
if let strongSelf = self {
overlayController?.animateOut(reclaim: true, targetPosition: strongSelf.controllerNode.actionButtonPosition, completion: { immediate in
if let strongSelf = self, immediate {
strongSelf.controllerNode.actionButton.ignoreHierarchyChanges = true
strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.cameraButton)
strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.audioButton)
strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.leaveButton)
strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.actionButton)
if immediate, let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
}
strongSelf.controllerNode.actionButton.ignoreHierarchyChanges = false
}
})
strongSelf.reclaimActionButton = nil
}
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
self.didAppearOnce = false
self.controllerNode.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
})
DispatchQueue.main.async {
self.onViewDidDisappear?()
}
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
private final class VoiceChatContextExtractedContentSource: ContextExtractedContentSource {
var keepInPlace: Bool
let ignoreContentTouches: Bool = false
let blurBackground: Bool
let maskView: UIView?
private var animateTransitionIn: () -> Void
private var animateTransitionOut: () -> Void
private let sourceNode: ContextExtractedContentContainingNode
var centerVertically: Bool
var shouldBeDismissed: Signal<Bool, NoError>
init(sourceNode: ContextExtractedContentContainingNode, maskView: UIView?, keepInPlace: Bool, blurBackground: Bool, centerVertically: Bool, shouldBeDismissed: Signal<Bool, NoError>, animateTransitionIn: @escaping () -> Void, animateTransitionOut: @escaping () -> Void) {
self.sourceNode = sourceNode
self.maskView = maskView
self.keepInPlace = keepInPlace
self.blurBackground = blurBackground
self.centerVertically = centerVertically
self.shouldBeDismissed = shouldBeDismissed
self.animateTransitionIn = animateTransitionIn
self.animateTransitionOut = animateTransitionOut
}
func takeView() -> ContextControllerTakeViewInfo? {
self.animateTransitionIn()
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds, maskView: self.maskView)
}
func putBack() -> ContextControllerPutBackViewInfo? {
self.animateTransitionOut()
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds, maskView: self.maskView)
}
}
private final class VoiceChatContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ContextReferenceContentNode
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}