mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Video chat UI
This commit is contained in:
parent
001755ad63
commit
5e356fd7b6
@ -5,19 +5,54 @@ import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import TelegramPresentationData
|
||||
import AppBundle
|
||||
import TelegramAudio
|
||||
|
||||
final class VideoChatActionButtonComponent: Component {
|
||||
enum Content: Equatable {
|
||||
fileprivate enum IconType {
|
||||
enum BluetoothType: Equatable {
|
||||
case generic
|
||||
case airpods
|
||||
case airpodsPro
|
||||
case airpodsMax
|
||||
}
|
||||
|
||||
enum Audio: Equatable {
|
||||
case none
|
||||
case builtin
|
||||
case speaker
|
||||
case headphones
|
||||
case bluetooth(BluetoothType)
|
||||
}
|
||||
|
||||
fileprivate enum IconType: Equatable {
|
||||
enum Audio: Equatable {
|
||||
case speaker
|
||||
case headphones
|
||||
case bluetooth(BluetoothType)
|
||||
}
|
||||
|
||||
case audio(audio: Audio)
|
||||
case video
|
||||
case leave
|
||||
}
|
||||
|
||||
case audio(audio: Audio)
|
||||
case video(isActive: Bool)
|
||||
case leave
|
||||
|
||||
fileprivate var iconType: IconType {
|
||||
switch self {
|
||||
case let .audio(audio):
|
||||
let mappedAudio: IconType.Audio
|
||||
switch audio {
|
||||
case .none, .builtin, .speaker:
|
||||
mappedAudio = .speaker
|
||||
case .headphones:
|
||||
mappedAudio = .headphones
|
||||
case let .bluetooth(type):
|
||||
mappedAudio = .bluetooth(type)
|
||||
}
|
||||
return .audio(audio: mappedAudio)
|
||||
case .video:
|
||||
return .video
|
||||
case .leave:
|
||||
@ -30,23 +65,30 @@ final class VideoChatActionButtonComponent: Component {
|
||||
case connecting
|
||||
case muted
|
||||
case unmuted
|
||||
case raiseHand
|
||||
}
|
||||
|
||||
let strings: PresentationStrings
|
||||
let content: Content
|
||||
let microphoneState: MicrophoneState
|
||||
let isCollapsed: Bool
|
||||
|
||||
init(
|
||||
strings: PresentationStrings,
|
||||
content: Content,
|
||||
microphoneState: MicrophoneState,
|
||||
isCollapsed: Bool
|
||||
) {
|
||||
self.strings = strings
|
||||
self.content = content
|
||||
self.microphoneState = microphoneState
|
||||
self.isCollapsed = isCollapsed
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatActionButtonComponent, rhs: VideoChatActionButtonComponent) -> Bool {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
@ -94,6 +136,30 @@ final class VideoChatActionButtonComponent: Component {
|
||||
let backgroundColor: UIColor
|
||||
let iconDiameter: CGFloat
|
||||
switch component.content {
|
||||
case let .audio(audio):
|
||||
var isActive = false
|
||||
switch audio {
|
||||
case .none, .builtin:
|
||||
titleText = component.strings.Call_Speaker
|
||||
case .speaker:
|
||||
isActive = true
|
||||
titleText = component.strings.Call_Speaker
|
||||
case .headphones:
|
||||
titleText = component.strings.Call_Audio
|
||||
case .bluetooth:
|
||||
titleText = component.strings.Call_Audio
|
||||
}
|
||||
switch component.microphoneState {
|
||||
case .connecting:
|
||||
backgroundColor = UIColor(white: 0.1, alpha: 1.0)
|
||||
case .muted:
|
||||
backgroundColor = !isActive ? UIColor(rgb: 0x002E5D) : UIColor(rgb: 0x027FFF)
|
||||
case .unmuted:
|
||||
backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659)
|
||||
case .raiseHand:
|
||||
backgroundColor = UIColor(rgb: 0x3252EF)
|
||||
}
|
||||
iconDiameter = 60.0
|
||||
case let .video(isActive):
|
||||
titleText = "video"
|
||||
switch component.microphoneState {
|
||||
@ -103,6 +169,8 @@ final class VideoChatActionButtonComponent: Component {
|
||||
backgroundColor = !isActive ? UIColor(rgb: 0x002E5D) : UIColor(rgb: 0x027FFF)
|
||||
case .unmuted:
|
||||
backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659)
|
||||
case .raiseHand:
|
||||
backgroundColor = UIColor(rgb: 0x3252EF)
|
||||
}
|
||||
iconDiameter = 60.0
|
||||
case .leave:
|
||||
@ -113,6 +181,26 @@ final class VideoChatActionButtonComponent: Component {
|
||||
|
||||
if self.contentImage == nil || previousComponent?.content.iconType != component.content.iconType {
|
||||
switch component.content.iconType {
|
||||
case let .audio(audio):
|
||||
let iconName: String
|
||||
switch audio {
|
||||
case .speaker:
|
||||
iconName = "Call/CallSpeakerButton"
|
||||
case .headphones:
|
||||
iconName = "Call/CallHeadphonesButton"
|
||||
case let .bluetooth(type):
|
||||
switch type {
|
||||
case .generic:
|
||||
iconName = "Call/CallBluetoothButton"
|
||||
case .airpods:
|
||||
iconName = "Call/CallAirpodsButton"
|
||||
case .airpodsPro:
|
||||
iconName = "Call/CallAirpodsProButton"
|
||||
case .airpodsMax:
|
||||
iconName = "Call/CallAirpodsMaxButton"
|
||||
}
|
||||
}
|
||||
self.contentImage = UIImage(bundleImageName: iconName)?.precomposed().withRenderingMode(.alwaysTemplate)
|
||||
case .video:
|
||||
self.contentImage = UIImage(bundleImageName: "Call/CallCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate)
|
||||
case .leave:
|
||||
|
@ -179,23 +179,27 @@ final class VideoChatMicButtonComponent: Component {
|
||||
case connecting
|
||||
case muted
|
||||
case unmuted(pushToTalk: Bool)
|
||||
case raiseHand
|
||||
}
|
||||
|
||||
let call: PresentationGroupCall
|
||||
let content: Content
|
||||
let isCollapsed: Bool
|
||||
let updateUnmutedStateIsPushToTalk: (Bool?) -> Void
|
||||
let raiseHand: () -> Void
|
||||
|
||||
init(
|
||||
call: PresentationGroupCall,
|
||||
content: Content,
|
||||
isCollapsed: Bool,
|
||||
updateUnmutedStateIsPushToTalk: @escaping (Bool?) -> Void
|
||||
updateUnmutedStateIsPushToTalk: @escaping (Bool?) -> Void,
|
||||
raiseHand: @escaping () -> Void
|
||||
) {
|
||||
self.call = call
|
||||
self.content = content
|
||||
self.isCollapsed = isCollapsed
|
||||
self.updateUnmutedStateIsPushToTalk = updateUnmutedStateIsPushToTalk
|
||||
self.raiseHand = raiseHand
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatMicButtonComponent, rhs: VideoChatMicButtonComponent) -> Bool {
|
||||
@ -241,13 +245,11 @@ final class VideoChatMicButtonComponent: Component {
|
||||
self.beginTrackingTimestamp = CFAbsoluteTimeGetCurrent()
|
||||
if let component = self.component {
|
||||
switch component.content {
|
||||
case .connecting:
|
||||
case .connecting, .unmuted, .raiseHand:
|
||||
self.beginTrackingWasPushToTalk = false
|
||||
case .muted:
|
||||
self.beginTrackingWasPushToTalk = true
|
||||
component.updateUnmutedStateIsPushToTalk(true)
|
||||
case .unmuted:
|
||||
self.beginTrackingWasPushToTalk = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,6 +287,10 @@ final class VideoChatMicButtonComponent: Component {
|
||||
} else {
|
||||
component.updateUnmutedStateIsPushToTalk(nil)
|
||||
}
|
||||
case .raiseHand:
|
||||
self.icon.playRandomAnimation()
|
||||
|
||||
component.raiseHand()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -314,6 +320,8 @@ final class VideoChatMicButtonComponent: Component {
|
||||
titleText = "Unmute"
|
||||
case let .unmuted(isPushToTalk):
|
||||
titleText = isPushToTalk ? "You are Live" : "Tap to Mute"
|
||||
case .raiseHand:
|
||||
titleText = "Raise Hand"
|
||||
}
|
||||
self.isEnabled = isEnabled
|
||||
|
||||
@ -382,10 +390,12 @@ final class VideoChatMicButtonComponent: Component {
|
||||
case .connecting:
|
||||
context.setFillColor(UIColor(white: 0.1, alpha: 1.0).cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
case .muted, .unmuted:
|
||||
case .muted, .unmuted, .raiseHand:
|
||||
let colors: [UIColor]
|
||||
if case .muted = component.content {
|
||||
colors = [UIColor(rgb: 0x0080FF), UIColor(rgb: 0x00A1FE)]
|
||||
} else if case .raiseHand = component.content {
|
||||
colors = [UIColor(rgb: 0x3252EF), UIColor(rgb: 0xC64688)]
|
||||
} else {
|
||||
colors = [UIColor(rgb: 0x33C659), UIColor(rgb: 0x0BA8A5)]
|
||||
}
|
||||
@ -465,10 +475,12 @@ final class VideoChatMicButtonComponent: Component {
|
||||
self.icon.enqueueState(.mute)
|
||||
case .unmuted:
|
||||
self.icon.enqueueState(.unmute)
|
||||
case .raiseHand:
|
||||
self.icon.enqueueState(.hand)
|
||||
}
|
||||
|
||||
switch component.content {
|
||||
case .muted, .unmuted:
|
||||
case .muted, .unmuted, .raiseHand:
|
||||
let blobSize = CGRect(origin: CGPoint(), size: CGSize(width: 116.0, height: 116.0)).insetBy(dx: -40.0, dy: -40.0).size
|
||||
|
||||
let blobTintTransition: ComponentTransition
|
||||
@ -495,7 +507,15 @@ final class VideoChatMicButtonComponent: Component {
|
||||
transition.setPosition(view: blobView, position: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5))
|
||||
transition.setScale(view: blobView, scale: availableSize.width / 116.0)
|
||||
|
||||
blobTintTransition.setTintColor(layer: blobView.blobsLayer, color: component.content == .muted ? UIColor(rgb: 0x0086FF) : UIColor(rgb: 0x33C758))
|
||||
let blobsColor: UIColor
|
||||
if case .muted = component.content {
|
||||
blobsColor = UIColor(rgb: 0x0086FF)
|
||||
} else if case .raiseHand = component.content {
|
||||
blobsColor = UIColor(rgb: 0x914BAD)
|
||||
} else {
|
||||
blobsColor = UIColor(rgb: 0x33C758)
|
||||
}
|
||||
blobTintTransition.setTintColor(layer: blobView.blobsLayer, color: blobsColor)
|
||||
|
||||
switch component.content {
|
||||
case .unmuted:
|
||||
@ -508,7 +528,7 @@ final class VideoChatMicButtonComponent: Component {
|
||||
blobView.updateLevel(CGFloat(value), immediately: false)
|
||||
})
|
||||
}
|
||||
case .connecting, .muted:
|
||||
case .connecting, .muted, .raiseHand:
|
||||
if let audioLevelDisposable = self.audioLevelDisposable {
|
||||
self.audioLevelDisposable = nil
|
||||
audioLevelDisposable.dispose()
|
||||
@ -536,7 +556,14 @@ final class VideoChatMicButtonComponent: Component {
|
||||
glowView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
let glowColor: UIColor = component.content == .muted ? UIColor(rgb: 0x0086FF) : UIColor(rgb: 0x33C758)
|
||||
let glowColor: UIColor
|
||||
if case .muted = component.content {
|
||||
glowColor = UIColor(rgb: 0x0086FF)
|
||||
} else if case .raiseHand = component.content {
|
||||
glowColor = UIColor(rgb: 0x3252EF)
|
||||
} else {
|
||||
glowColor = UIColor(rgb: 0x33C758)
|
||||
}
|
||||
glowView.update(size: glowFrame.size, color: glowColor.withMultipliedAlpha(component.isCollapsed ? 0.5 : 0.7), transition: transition, colorTransition: blobTintTransition)
|
||||
transition.setFrame(view: glowView, frame: glowFrame)
|
||||
default:
|
||||
|
@ -4,18 +4,22 @@ import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import TelegramCore
|
||||
import LottieComponent
|
||||
|
||||
final class VideoChatParticipantStatusComponent: Component {
|
||||
let muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
let hasRaiseHand: Bool
|
||||
let isSpeaking: Bool
|
||||
let theme: PresentationTheme
|
||||
|
||||
init(
|
||||
muteState: GroupCallParticipantsContext.Participant.MuteState?,
|
||||
hasRaiseHand: Bool,
|
||||
isSpeaking: Bool,
|
||||
theme: PresentationTheme
|
||||
) {
|
||||
self.muteState = muteState
|
||||
self.hasRaiseHand = hasRaiseHand
|
||||
self.isSpeaking = isSpeaking
|
||||
self.theme = theme
|
||||
}
|
||||
@ -24,6 +28,9 @@ final class VideoChatParticipantStatusComponent: Component {
|
||||
if lhs.muteState != rhs.muteState {
|
||||
return false
|
||||
}
|
||||
if lhs.hasRaiseHand != rhs.hasRaiseHand {
|
||||
return false
|
||||
}
|
||||
if lhs.isSpeaking != rhs.isSpeaking {
|
||||
return false
|
||||
}
|
||||
@ -34,7 +41,8 @@ final class VideoChatParticipantStatusComponent: Component {
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let muteStatus = ComponentView<Empty>()
|
||||
private var muteStatus: ComponentView<Empty>?
|
||||
private var raiseHandStatus: ComponentView<Empty>?
|
||||
|
||||
private var component: VideoChatParticipantStatusComponent?
|
||||
private var isUpdating: Bool = false
|
||||
@ -56,47 +64,149 @@ final class VideoChatParticipantStatusComponent: Component {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
let alphaTransition: ComponentTransition
|
||||
if !transition.animation.isImmediate {
|
||||
alphaTransition = .easeInOut(duration: 0.2)
|
||||
} else {
|
||||
alphaTransition = .immediate
|
||||
}
|
||||
|
||||
let size = CGSize(width: 44.0, height: 44.0)
|
||||
|
||||
let muteStatusSize = self.muteStatus.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(VideoChatMuteIconComponent(
|
||||
color: .white,
|
||||
content: .mute(isFilled: false, isMuted: component.muteState != nil && !component.isSpeaking)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 36.0, height: 36.0)
|
||||
)
|
||||
let muteStatusFrame = CGRect(origin: CGPoint(x: floor((size.width - muteStatusSize.width) * 0.5), y: floor((size.height - muteStatusSize.height) * 0.5)), size: muteStatusSize)
|
||||
if let muteStatusView = self.muteStatus.view as? VideoChatMuteIconComponent.View {
|
||||
if muteStatusView.superview == nil {
|
||||
self.addSubview(muteStatusView)
|
||||
}
|
||||
transition.setFrame(view: muteStatusView, frame: muteStatusFrame)
|
||||
|
||||
let tintTransition: ComponentTransition
|
||||
if !transition.animation.isImmediate {
|
||||
tintTransition = .easeInOut(duration: 0.2)
|
||||
let isRaiseHand: Bool
|
||||
if let muteState = component.muteState {
|
||||
if muteState.canUnmute {
|
||||
isRaiseHand = false
|
||||
} else {
|
||||
tintTransition = .immediate
|
||||
isRaiseHand = component.hasRaiseHand
|
||||
}
|
||||
if let iconView = muteStatusView.iconView {
|
||||
let iconTintColor: UIColor
|
||||
if component.isSpeaking {
|
||||
iconTintColor = UIColor(rgb: 0x33C758)
|
||||
} else {
|
||||
isRaiseHand = false
|
||||
}
|
||||
|
||||
if !isRaiseHand {
|
||||
let muteStatus: ComponentView<Empty>
|
||||
var muteStatusTransition = transition
|
||||
if let current = self.muteStatus {
|
||||
muteStatus = current
|
||||
} else {
|
||||
muteStatusTransition = muteStatusTransition.withAnimation(.none)
|
||||
muteStatus = ComponentView()
|
||||
self.muteStatus = muteStatus
|
||||
}
|
||||
|
||||
let muteStatusSize = muteStatus.update(
|
||||
transition: muteStatusTransition,
|
||||
component: AnyComponent(VideoChatMuteIconComponent(
|
||||
color: .white,
|
||||
content: .mute(isFilled: false, isMuted: component.muteState != nil && !component.isSpeaking)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 36.0, height: 36.0)
|
||||
)
|
||||
let muteStatusFrame = CGRect(origin: CGPoint(x: floor((size.width - muteStatusSize.width) * 0.5), y: floor((size.height - muteStatusSize.height) * 0.5)), size: muteStatusSize)
|
||||
if let muteStatusView = muteStatus.view as? VideoChatMuteIconComponent.View {
|
||||
var animateIn = false
|
||||
if muteStatusView.superview == nil {
|
||||
animateIn = true
|
||||
self.addSubview(muteStatusView)
|
||||
}
|
||||
muteStatusTransition.setFrame(view: muteStatusView, frame: muteStatusFrame)
|
||||
|
||||
let tintTransition: ComponentTransition
|
||||
if !muteStatusTransition.animation.isImmediate {
|
||||
tintTransition = .easeInOut(duration: 0.2)
|
||||
} else {
|
||||
if let muteState = component.muteState {
|
||||
if muteState.canUnmute {
|
||||
iconTintColor = UIColor(white: 1.0, alpha: 0.4)
|
||||
} else {
|
||||
iconTintColor = UIColor(rgb: 0xFF3B30)
|
||||
}
|
||||
tintTransition = .immediate
|
||||
}
|
||||
if let iconView = muteStatusView.iconView {
|
||||
let iconTintColor: UIColor
|
||||
if component.isSpeaking {
|
||||
iconTintColor = UIColor(rgb: 0x33C758)
|
||||
} else {
|
||||
iconTintColor = UIColor(white: 1.0, alpha: 0.4)
|
||||
if let muteState = component.muteState {
|
||||
if muteState.canUnmute {
|
||||
iconTintColor = UIColor(white: 1.0, alpha: 0.4)
|
||||
} else {
|
||||
iconTintColor = UIColor(rgb: 0xFF3B30)
|
||||
}
|
||||
} else {
|
||||
iconTintColor = UIColor(white: 1.0, alpha: 0.4)
|
||||
}
|
||||
}
|
||||
|
||||
tintTransition.setTintColor(layer: iconView.layer, color: iconTintColor)
|
||||
}
|
||||
|
||||
tintTransition.setTintColor(layer: iconView.layer, color: iconTintColor)
|
||||
if animateIn, !transition.animation.isImmediate {
|
||||
transition.animateScale(view: muteStatusView, from: 0.001, to: 1.0)
|
||||
alphaTransition.animateAlpha(view: muteStatusView, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
} else if let muteStatus = self.muteStatus {
|
||||
self.muteStatus = nil
|
||||
|
||||
if let muteStatusView = muteStatus.view {
|
||||
if !transition.animation.isImmediate {
|
||||
transition.setScale(view: muteStatusView, scale: 0.001)
|
||||
alphaTransition.setAlpha(view: muteStatusView, alpha: 0.0, completion: { [weak muteStatusView] _ in
|
||||
muteStatusView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
muteStatusView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isRaiseHand {
|
||||
let raiseHandStatus: ComponentView<Empty>
|
||||
var raiseHandStatusTransition = transition
|
||||
if let current = self.raiseHandStatus {
|
||||
raiseHandStatus = current
|
||||
} else {
|
||||
raiseHandStatusTransition = raiseHandStatusTransition.withAnimation(.none)
|
||||
raiseHandStatus = ComponentView()
|
||||
self.raiseHandStatus = raiseHandStatus
|
||||
}
|
||||
|
||||
let raiseHandStatusSize = raiseHandStatus.update(
|
||||
transition: raiseHandStatusTransition,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(
|
||||
name: "anim_hand1"
|
||||
),
|
||||
color: component.theme.list.itemAccentColor,
|
||||
size: CGSize(width: 48.0, height: 48.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 48.0, height: 48.0)
|
||||
)
|
||||
let raiseHandStatusFrame = CGRect(origin: CGPoint(x: floor((size.width - raiseHandStatusSize.width) * 0.5) - 2.0, y: floor((size.height - raiseHandStatusSize.height) * 0.5)), size: raiseHandStatusSize)
|
||||
if let raiseHandStatusView = raiseHandStatus.view {
|
||||
var animateIn = false
|
||||
if raiseHandStatusView.superview == nil {
|
||||
animateIn = true
|
||||
self.addSubview(raiseHandStatusView)
|
||||
}
|
||||
raiseHandStatusTransition.setFrame(view: raiseHandStatusView, frame: raiseHandStatusFrame)
|
||||
|
||||
if animateIn, !transition.animation.isImmediate {
|
||||
transition.animateScale(view: raiseHandStatusView, from: 0.001, to: 1.0)
|
||||
alphaTransition.animateAlpha(view: raiseHandStatusView, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
} else if let raiseHandStatus = self.raiseHandStatus {
|
||||
self.raiseHandStatus = nil
|
||||
|
||||
if let raiseHandStatusView = raiseHandStatus.view {
|
||||
if !transition.animation.isImmediate {
|
||||
transition.setScale(view: raiseHandStatusView, scale: 0.001)
|
||||
alphaTransition.setAlpha(view: raiseHandStatusView, alpha: 0.0, completion: { [weak raiseHandStatusView] _ in
|
||||
raiseHandStatusView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
raiseHandStatusView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,16 +35,23 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
|
||||
final class Participants: Equatable {
|
||||
enum InviteType {
|
||||
case invite
|
||||
case shareLink
|
||||
}
|
||||
|
||||
let myPeerId: EnginePeer.Id
|
||||
let participants: [GroupCallParticipantsContext.Participant]
|
||||
let totalCount: Int
|
||||
let loadMoreToken: String?
|
||||
let inviteType: InviteType?
|
||||
|
||||
init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?) {
|
||||
init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteType: InviteType?) {
|
||||
self.myPeerId = myPeerId
|
||||
self.participants = participants
|
||||
self.totalCount = totalCount
|
||||
self.loadMoreToken = loadMoreToken
|
||||
self.inviteType = inviteType
|
||||
}
|
||||
|
||||
static func ==(lhs: Participants, rhs: Participants) -> Bool {
|
||||
@ -63,6 +70,9 @@ final class VideoChatParticipantsComponent: Component {
|
||||
if lhs.loadMoreToken != rhs.loadMoreToken {
|
||||
return false
|
||||
}
|
||||
if lhs.inviteType != rhs.inviteType {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -1046,6 +1056,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
|
||||
let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantStatusComponent(
|
||||
muteState: participant.muteState,
|
||||
hasRaiseHand: participant.hasRaiseHand,
|
||||
isSpeaking: component.speakingParticipants.contains(participant.peer.id),
|
||||
theme: component.theme
|
||||
))
|
||||
@ -1393,10 +1404,21 @@ final class VideoChatParticipantsComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
|
||||
let inviteText: String
|
||||
if let participants = component.participants, let inviteType = participants.inviteType {
|
||||
switch inviteType {
|
||||
case .invite:
|
||||
inviteText = "Invite Members"
|
||||
case .shareLink:
|
||||
inviteText = "Share Invite Link"
|
||||
}
|
||||
} else {
|
||||
inviteText = "Invite Members"
|
||||
}
|
||||
let inviteListItemSize = self.inviteListItemView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(VideoChatListInviteComponent(
|
||||
title: "Invite Members",
|
||||
title: inviteText,
|
||||
theme: component.theme,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
|
@ -75,6 +75,7 @@ private final class VideoChatScreenComponent: Component {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let navigationLeftButton = ComponentView<Empty>()
|
||||
private let navigationRightButton = ComponentView<Empty>()
|
||||
private var navigationSidebarButton: ComponentView<Empty>?
|
||||
|
||||
private let videoButton = ComponentView<Empty>()
|
||||
private let leaveButton = ComponentView<Empty>()
|
||||
@ -1432,298 +1433,332 @@ private final class VideoChatScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
let groupPeer = component.call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.call.peerId))
|
||||
let _ = (groupPeer
|
||||
|> deliverOnMainQueue).start(next: { [weak self] groupPeer in
|
||||
guard let self, let component = self.component, let environment = self.environment, let groupPeer else {
|
||||
return
|
||||
}
|
||||
let inviteLinks = self.inviteLinks
|
||||
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
var canInviteMembers = true
|
||||
if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) {
|
||||
canInviteMembers = false
|
||||
}
|
||||
if !canInviteMembers {
|
||||
if let inviteLinks {
|
||||
self.presentShare(inviteLinks)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var filters: [ChannelMembersSearchFilter] = []
|
||||
if let members = self.members {
|
||||
filters.append(.disable(Array(members.participants.map { $0.peer.id })))
|
||||
}
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil {
|
||||
filters.append(.excludeNonMembers)
|
||||
}
|
||||
} else if case let .legacyGroup(groupPeer) = groupPeer {
|
||||
if groupPeer.hasBannedPermission(.banAddMembers) {
|
||||
filters.append(.excludeNonMembers)
|
||||
}
|
||||
}
|
||||
filters.append(.excludeBots)
|
||||
|
||||
var dismissController: (() -> Void)?
|
||||
let controller = ChannelMembersSearchController(context: component.call.accountContext, peerId: groupPeer.id, forceTheme: environment.theme, mode: .inviteToCall, filters: filters, openPeer: { [weak self] peer, participant in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
guard let callState = self.callState else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
||||
if peer.id == callState.myPeerId {
|
||||
return
|
||||
}
|
||||
if let participant {
|
||||
dismissController?()
|
||||
|
||||
if component.call.invitePeer(participant.peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
var canInvite = true
|
||||
var inviteIsLink = false
|
||||
if case let .channel(peer) = self.peer {
|
||||
if peer.flags.contains(.isGigagroup) {
|
||||
if peer.flags.contains(.isCreator) || peer.adminRights != nil {
|
||||
} else {
|
||||
if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) {
|
||||
let text = environment.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
|
||||
environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in
|
||||
dismissController?()
|
||||
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (enqueueMessages(account: component.call.accountContext.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true })
|
||||
})
|
||||
})]), in: .window(.root))
|
||||
} else {
|
||||
let text: String
|
||||
if case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info {
|
||||
text = environment.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), groupPeer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
}
|
||||
|
||||
environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
guard let selfController = environment.controller() else {
|
||||
return
|
||||
}
|
||||
let inviteDisposable = self.inviteDisposable
|
||||
var inviteSignal = component.call.accountContext.peerChannelMemberCategoriesContextsManager.addMembers(engine: component.call.accountContext.engine, peerId: groupPeer.id, memberIds: [peer.id])
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
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: { [weak self] error in
|
||||
dismissController?()
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
|
||||
let text: String
|
||||
switch error {
|
||||
case .limitExceeded:
|
||||
text = environment.strings.Channel_ErrorAddTooMuch
|
||||
case .tooMuchJoined:
|
||||
text = environment.strings.Invite_ChannelsTooMuch
|
||||
case .generic:
|
||||
text = environment.strings.Login_UnknownError
|
||||
case .restricted:
|
||||
text = environment.strings.Channel_ErrorAddBlocked
|
||||
case .notMutualContact:
|
||||
if case .broadcast = groupPeer.info {
|
||||
text = environment.strings.Channel_AddUserLeftError
|
||||
} else {
|
||||
text = environment.strings.GroupInfo_AddUserLeftError
|
||||
}
|
||||
case .botDoesntSupportGroups:
|
||||
text = environment.strings.Channel_BotDoesntSupportGroups
|
||||
case .tooMuchBots:
|
||||
text = environment.strings.Channel_TooMuchBots
|
||||
case .bot:
|
||||
text = environment.strings.Login_UnknownError
|
||||
case .kicked:
|
||||
text = environment.strings.Channel_AddUserKickedError
|
||||
}
|
||||
environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, completed: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if component.call.invitePeer(peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
}))
|
||||
} else if case let .legacyGroup(groupPeer) = groupPeer {
|
||||
guard let selfController = environment.controller() else {
|
||||
return
|
||||
}
|
||||
let inviteDisposable = self.inviteDisposable
|
||||
var inviteSignal = component.call.accountContext.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id)
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
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: { [weak self] error in
|
||||
dismissController?()
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
let context = component.call.accountContext
|
||||
|
||||
switch error {
|
||||
case .privacy:
|
||||
let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(peer.id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
environment.controller()?.present(textAlertController(context: component.call.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
})
|
||||
case .notMutualContact:
|
||||
environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
case .tooManyChannels:
|
||||
environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
case .groupFull, .generic:
|
||||
environment.controller()?.present(textAlertController(context: context, forceTheme: environment.theme, title: nil, text: environment.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}, completed: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if component.call.invitePeer(peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
}))
|
||||
}
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
canInvite = false
|
||||
}
|
||||
})
|
||||
controller.copyInviteLink = { [weak self] in
|
||||
dismissController?()
|
||||
|
||||
guard let self, let component = self.component else {
|
||||
}
|
||||
if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) {
|
||||
inviteIsLink = true
|
||||
}
|
||||
}
|
||||
var inviteType: VideoChatParticipantsComponent.Participants.InviteType?
|
||||
if canInvite {
|
||||
if inviteIsLink {
|
||||
inviteType = .shareLink
|
||||
} else {
|
||||
inviteType = .invite
|
||||
}
|
||||
}
|
||||
|
||||
guard let inviteType else {
|
||||
return
|
||||
}
|
||||
|
||||
switch inviteType {
|
||||
case .invite:
|
||||
let groupPeer = component.call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.call.peerId))
|
||||
let _ = (groupPeer
|
||||
|> deliverOnMainQueue).start(next: { [weak self] groupPeer in
|
||||
guard let self, let component = self.component, let environment = self.environment, let groupPeer else {
|
||||
return
|
||||
}
|
||||
let callPeerId = component.call.peerId
|
||||
let inviteLinks = self.inviteLinks
|
||||
|
||||
let _ = (component.call.accountContext.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId),
|
||||
TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId)
|
||||
)
|
||||
|> map { peer, exportedInvitation -> String? in
|
||||
if let link = inviteLinks?.listenerLink {
|
||||
return link
|
||||
} else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty {
|
||||
return "https://t.me/\(addressName)"
|
||||
} else if let link = exportedInvitation?.link {
|
||||
return link
|
||||
} else {
|
||||
return nil
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
var canInviteMembers = true
|
||||
if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) {
|
||||
canInviteMembers = false
|
||||
}
|
||||
if !canInviteMembers {
|
||||
if let inviteLinks {
|
||||
self.presentShare(inviteLinks)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] link in
|
||||
guard let self, let environment = self.environment else {
|
||||
|
||||
var filters: [ChannelMembersSearchFilter] = []
|
||||
if let members = self.members {
|
||||
filters.append(.disable(Array(members.participants.map { $0.peer.id })))
|
||||
}
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil {
|
||||
filters.append(.excludeNonMembers)
|
||||
}
|
||||
} else if case let .legacyGroup(groupPeer) = groupPeer {
|
||||
if groupPeer.hasBannedPermission(.banAddMembers) {
|
||||
filters.append(.excludeNonMembers)
|
||||
}
|
||||
}
|
||||
filters.append(.excludeBots)
|
||||
|
||||
var dismissController: (() -> Void)?
|
||||
let controller = ChannelMembersSearchController(context: component.call.accountContext, peerId: groupPeer.id, forceTheme: environment.theme, mode: .inviteToCall, filters: filters, openPeer: { [weak self] peer, participant in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
guard let callState = self.callState else {
|
||||
return
|
||||
}
|
||||
|
||||
if let link {
|
||||
UIPasteboard.general.string = link
|
||||
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
||||
if peer.id == callState.myPeerId {
|
||||
return
|
||||
}
|
||||
if let participant {
|
||||
dismissController?()
|
||||
|
||||
self.presentUndoOverlay(content: .linkCopied(text: environment.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false })
|
||||
if component.call.invitePeer(participant.peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
} else {
|
||||
if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) {
|
||||
let text = environment.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
|
||||
environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in
|
||||
dismissController?()
|
||||
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (enqueueMessages(account: component.call.accountContext.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true })
|
||||
})
|
||||
})]), in: .window(.root))
|
||||
} else {
|
||||
let text: String
|
||||
if case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info {
|
||||
text = environment.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), groupPeer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
}
|
||||
|
||||
environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
guard let selfController = environment.controller() else {
|
||||
return
|
||||
}
|
||||
let inviteDisposable = self.inviteDisposable
|
||||
var inviteSignal = component.call.accountContext.peerChannelMemberCategoriesContextsManager.addMembers(engine: component.call.accountContext.engine, peerId: groupPeer.id, memberIds: [peer.id])
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
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: { [weak self] error in
|
||||
dismissController?()
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
|
||||
let text: String
|
||||
switch error {
|
||||
case .limitExceeded:
|
||||
text = environment.strings.Channel_ErrorAddTooMuch
|
||||
case .tooMuchJoined:
|
||||
text = environment.strings.Invite_ChannelsTooMuch
|
||||
case .generic:
|
||||
text = environment.strings.Login_UnknownError
|
||||
case .restricted:
|
||||
text = environment.strings.Channel_ErrorAddBlocked
|
||||
case .notMutualContact:
|
||||
if case .broadcast = groupPeer.info {
|
||||
text = environment.strings.Channel_AddUserLeftError
|
||||
} else {
|
||||
text = environment.strings.GroupInfo_AddUserLeftError
|
||||
}
|
||||
case .botDoesntSupportGroups:
|
||||
text = environment.strings.Channel_BotDoesntSupportGroups
|
||||
case .tooMuchBots:
|
||||
text = environment.strings.Channel_TooMuchBots
|
||||
case .bot:
|
||||
text = environment.strings.Login_UnknownError
|
||||
case .kicked:
|
||||
text = environment.strings.Channel_AddUserKickedError
|
||||
}
|
||||
environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, completed: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if component.call.invitePeer(peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
}))
|
||||
} else if case let .legacyGroup(groupPeer) = groupPeer {
|
||||
guard let selfController = environment.controller() else {
|
||||
return
|
||||
}
|
||||
let inviteDisposable = self.inviteDisposable
|
||||
var inviteSignal = component.call.accountContext.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id)
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
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: { [weak self] error in
|
||||
dismissController?()
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
let context = component.call.accountContext
|
||||
|
||||
switch error {
|
||||
case .privacy:
|
||||
let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(peer.id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
environment.controller()?.present(textAlertController(context: component.call.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
})
|
||||
case .notMutualContact:
|
||||
environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
case .tooManyChannels:
|
||||
environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
case .groupFull, .generic:
|
||||
environment.controller()?.present(textAlertController(context: context, forceTheme: environment.theme, title: nil, text: environment.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}, completed: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if component.call.invitePeer(peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
}))
|
||||
}
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
})
|
||||
controller.copyInviteLink = { [weak self] in
|
||||
dismissController?()
|
||||
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
let callPeerId = component.call.peerId
|
||||
|
||||
let _ = (component.call.accountContext.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId),
|
||||
TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId)
|
||||
)
|
||||
|> map { peer, exportedInvitation -> String? in
|
||||
if let link = inviteLinks?.listenerLink {
|
||||
return link
|
||||
} else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty {
|
||||
return "https://t.me/\(addressName)"
|
||||
} else if let link = exportedInvitation?.link {
|
||||
return link
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] link in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
|
||||
if let link {
|
||||
UIPasteboard.general.string = link
|
||||
|
||||
self.presentUndoOverlay(content: .linkCopied(text: environment.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false })
|
||||
}
|
||||
})
|
||||
}
|
||||
dismissController = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
environment.controller()?.push(controller)
|
||||
})
|
||||
case .shareLink:
|
||||
guard let inviteLinks = self.inviteLinks else {
|
||||
return
|
||||
}
|
||||
dismissController = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
environment.controller()?.push(controller)
|
||||
})
|
||||
self.presentShare(inviteLinks)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentShare(_ inviteLinks: GroupCallInviteLinks) {
|
||||
@ -1871,6 +1906,79 @@ private final class VideoChatScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private func onAudioRoutePressed() {
|
||||
guard let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
|
||||
HapticFeedback().impact(.light)
|
||||
|
||||
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 {
|
||||
component.call.setCurrentAudioOutput(output)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
||||
let actionSheet = ActionSheetController(presentationData: presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
for output in availableOutputs {
|
||||
let title: String
|
||||
var icon: UIImage?
|
||||
switch output {
|
||||
case .builtin:
|
||||
title = UIDevice.current.model
|
||||
case .speaker:
|
||||
title = environment.strings.Call_AudioRouteSpeaker
|
||||
icon = generateScaledImage(image: UIImage(bundleImageName: "Call/CallSpeakerButton"), size: CGSize(width: 48.0, height: 48.0), opaque: false)
|
||||
case .headphones:
|
||||
title = environment.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()
|
||||
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.call.setCurrentAudioOutput(output)
|
||||
}))
|
||||
}
|
||||
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: environment.strings.Call_AudioRouteHide, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
environment.controller()?.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: VideoChatScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
@ -1979,6 +2087,9 @@ private final class VideoChatScreenComponent: Component {
|
||||
|
||||
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, let members {
|
||||
if !expandedParticipantsVideoState.isMainParticipantPinned, let participant = members.participants.first(where: { participant in
|
||||
if let callState = self.callState, participant.peer.id == callState.myPeerId {
|
||||
return false
|
||||
}
|
||||
if participant.videoDescription != nil || participant.presentationDescription != nil {
|
||||
if members.speakingParticipants.contains(participant.peer.id) {
|
||||
return true
|
||||
@ -2140,6 +2251,51 @@ private final class VideoChatScreenComponent: Component {
|
||||
self.containerView.backgroundColor = .black
|
||||
}
|
||||
|
||||
var mappedParticipants: VideoChatParticipantsComponent.Participants?
|
||||
if let members = self.members, let callState = self.callState {
|
||||
var canInvite = true
|
||||
var inviteIsLink = false
|
||||
if case let .channel(peer) = self.peer {
|
||||
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
|
||||
}
|
||||
}
|
||||
var inviteType: VideoChatParticipantsComponent.Participants.InviteType?
|
||||
if canInvite {
|
||||
if inviteIsLink {
|
||||
inviteType = .shareLink
|
||||
} else {
|
||||
inviteType = .invite
|
||||
}
|
||||
}
|
||||
|
||||
mappedParticipants = VideoChatParticipantsComponent.Participants(
|
||||
myPeerId: callState.myPeerId,
|
||||
participants: members.participants,
|
||||
totalCount: members.totalCount,
|
||||
loadMoreToken: members.loadMoreToken,
|
||||
inviteType: inviteType
|
||||
)
|
||||
}
|
||||
|
||||
let maxSingleColumnWidth: CGFloat = 620.0
|
||||
let isTwoColumnLayout: Bool
|
||||
if availableSize.width > maxSingleColumnWidth {
|
||||
if let mappedParticipants, mappedParticipants.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) {
|
||||
isTwoColumnLayout = true
|
||||
} else {
|
||||
isTwoColumnLayout = false
|
||||
}
|
||||
} else {
|
||||
isTwoColumnLayout = false
|
||||
}
|
||||
|
||||
var containerOffset: CGFloat = 0.0
|
||||
if let panGestureState = self.panGestureState {
|
||||
containerOffset = panGestureState.offsetFraction * availableSize.height
|
||||
@ -2246,6 +2402,66 @@ private final class VideoChatScreenComponent: Component {
|
||||
transition.setFrame(view: navigationRightButtonView, frame: navigationRightButtonFrame)
|
||||
}
|
||||
|
||||
if isTwoColumnLayout, !"".isEmpty {
|
||||
var navigationSidebarButtonTransition = transition
|
||||
let navigationSidebarButton: ComponentView<Empty>
|
||||
if let current = self.navigationSidebarButton {
|
||||
navigationSidebarButton = current
|
||||
} else {
|
||||
navigationSidebarButtonTransition = navigationSidebarButtonTransition.withAnimation(.none)
|
||||
navigationSidebarButton = ComponentView()
|
||||
self.navigationSidebarButton = navigationSidebarButton
|
||||
}
|
||||
let navigationSidebarButtonSize = navigationSidebarButton.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Call/PanelIcon",
|
||||
tintColor: .white
|
||||
)),
|
||||
background: AnyComponent(Circle(
|
||||
fillColor: UIColor(white: 1.0, alpha: 0.1),
|
||||
size: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter)
|
||||
)),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let expandedParticipantsVideoState = self.expandedParticipantsVideoState {
|
||||
self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(
|
||||
mainParticipant: expandedParticipantsVideoState.mainParticipant,
|
||||
isMainParticipantPinned: expandedParticipantsVideoState.isMainParticipantPinned,
|
||||
isUIHidden: !expandedParticipantsVideoState.isUIHidden
|
||||
)
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter)
|
||||
)
|
||||
let navigationSidebarButtonFrame = CGRect(origin: CGPoint(x: navigationRightButtonFrame.minX - 32.0 - navigationSidebarButtonSize.width, y: topInset + floor((navigationBarHeight - navigationSidebarButtonSize.height) * 0.5)), size: navigationSidebarButtonSize)
|
||||
if let navigationSidebarButtonView = navigationSidebarButton.view {
|
||||
if navigationSidebarButtonView.superview == nil {
|
||||
if let navigationRightButtonView = self.navigationRightButton.view {
|
||||
self.containerView.insertSubview(navigationSidebarButtonView, aboveSubview: navigationRightButtonView)
|
||||
}
|
||||
}
|
||||
transition.setFrame(view: navigationSidebarButtonView, frame: navigationSidebarButtonFrame)
|
||||
}
|
||||
} else if let navigationSidebarButton = self.navigationSidebarButton {
|
||||
self.navigationSidebarButton = nil
|
||||
if let navigationSidebarButtonView = navigationSidebarButton.view {
|
||||
transition.setScale(view: navigationSidebarButtonView, scale: 0.001)
|
||||
transition.setAlpha(view: navigationSidebarButtonView, alpha: 0.0, completion: { [weak navigationSidebarButtonView] _ in
|
||||
navigationSidebarButtonView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let idleTitleStatusText: String
|
||||
if let callState = self.callState, callState.networkState == .connected, let members = self.members {
|
||||
idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount)))
|
||||
@ -2270,28 +2486,6 @@ private final class VideoChatScreenComponent: Component {
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
|
||||
var mappedParticipants: VideoChatParticipantsComponent.Participants?
|
||||
if let members = self.members, let callState = self.callState {
|
||||
mappedParticipants = VideoChatParticipantsComponent.Participants(
|
||||
myPeerId: callState.myPeerId,
|
||||
participants: members.participants,
|
||||
totalCount: members.totalCount,
|
||||
loadMoreToken: members.loadMoreToken
|
||||
)
|
||||
}
|
||||
|
||||
let maxSingleColumnWidth: CGFloat = 620.0
|
||||
let isTwoColumnLayout: Bool
|
||||
if availableSize.width > maxSingleColumnWidth {
|
||||
if let mappedParticipants, mappedParticipants.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) {
|
||||
isTwoColumnLayout = true
|
||||
} else {
|
||||
isTwoColumnLayout = false
|
||||
}
|
||||
} else {
|
||||
isTwoColumnLayout = false
|
||||
}
|
||||
|
||||
let areButtonsCollapsed: Bool
|
||||
let mainColumnWidth: CGFloat
|
||||
let mainColumnSideInset: CGFloat
|
||||
@ -2528,13 +2722,18 @@ private final class VideoChatScreenComponent: Component {
|
||||
micButtonContent = .connecting
|
||||
actionButtonMicrophoneState = .connecting
|
||||
case .connected:
|
||||
if let _ = callState.muteState {
|
||||
if self.isPushToTalkActive {
|
||||
micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive)
|
||||
actionButtonMicrophoneState = .unmuted
|
||||
if let callState = callState.muteState {
|
||||
if callState.canUnmute {
|
||||
if self.isPushToTalkActive {
|
||||
micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive)
|
||||
actionButtonMicrophoneState = .unmuted
|
||||
} else {
|
||||
micButtonContent = .muted
|
||||
actionButtonMicrophoneState = .muted
|
||||
}
|
||||
} else {
|
||||
micButtonContent = .muted
|
||||
actionButtonMicrophoneState = .muted
|
||||
micButtonContent = .raiseHand
|
||||
actionButtonMicrophoneState = .raiseHand
|
||||
}
|
||||
} else {
|
||||
micButtonContent = .unmuted(pushToTalk: false)
|
||||
@ -2587,6 +2786,17 @@ private final class VideoChatScreenComponent: Component {
|
||||
self.isPushToTalkActive = false
|
||||
self.state?.updated(transition: .spring(duration: 0.5))
|
||||
}
|
||||
},
|
||||
raiseHand: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let callState = self.callState else {
|
||||
return
|
||||
}
|
||||
if !callState.raisedHand {
|
||||
component.call.raiseHand()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -2600,11 +2810,44 @@ private final class VideoChatScreenComponent: Component {
|
||||
transition.setBounds(view: microphoneButtonView, bounds: CGRect(origin: CGPoint(), size: microphoneButtonFrame.size))
|
||||
}
|
||||
|
||||
let videoButtonContent: VideoChatActionButtonComponent.Content
|
||||
if let callState = self.callState, let muteState = callState.muteState, !muteState.canUnmute {
|
||||
var buttonAudio: VideoChatActionButtonComponent.Content.Audio = .speaker
|
||||
if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput {
|
||||
switch currentOutput {
|
||||
case .builtin:
|
||||
buttonAudio = .builtin
|
||||
case .speaker:
|
||||
buttonAudio = .speaker
|
||||
case .headphones:
|
||||
buttonAudio = .headphones
|
||||
case let .port(port):
|
||||
var type: VideoChatActionButtonComponent.Content.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
|
||||
}
|
||||
buttonAudio = .bluetooth(type)
|
||||
}
|
||||
if availableOutputs.count <= 1 {
|
||||
buttonAudio = .none
|
||||
}
|
||||
}
|
||||
videoButtonContent = .audio(audio: buttonAudio)
|
||||
} else {
|
||||
//TODO:release
|
||||
videoButtonContent = .video(isActive: false)
|
||||
}
|
||||
let _ = self.videoButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(VideoChatActionButtonComponent(
|
||||
content: .video(isActive: false),
|
||||
strings: environment.strings,
|
||||
content: videoButtonContent,
|
||||
microphoneState: actionButtonMicrophoneState,
|
||||
isCollapsed: areButtonsCollapsed
|
||||
)),
|
||||
@ -2613,7 +2856,11 @@ private final class VideoChatScreenComponent: Component {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.onCameraPressed()
|
||||
if let callState = self.callState, let muteState = callState.muteState, !muteState.canUnmute {
|
||||
self.onAudioRoutePressed()
|
||||
} else {
|
||||
self.onCameraPressed()
|
||||
}
|
||||
},
|
||||
animateAlpha: false
|
||||
)),
|
||||
@ -2632,6 +2879,7 @@ private final class VideoChatScreenComponent: Component {
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(VideoChatActionButtonComponent(
|
||||
strings: environment.strings,
|
||||
content: .leave,
|
||||
microphoneState: actionButtonMicrophoneState,
|
||||
isCollapsed: areButtonsCollapsed
|
||||
|
Loading…
x
Reference in New Issue
Block a user