diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index be259bce31..f69b179933 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -389,7 +389,10 @@ public extension GroupCallParticipantsContext.Participant { guard let videoDescription = self.videoDescription else { return nil } - return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, peerId: self.peer.id.id._internalGetInt64Value(), endpointId: videoDescription.endpointId, ssrcGroups: videoDescription.ssrcGroups.map { group in + guard let peer = self.peer else { + return nil + } + return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, peerId: peer.id.id._internalGetInt64Value(), endpointId: videoDescription.endpointId, ssrcGroups: videoDescription.ssrcGroups.map { group in PresentationGroupCallRequestedVideo.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs) }, minQuality: minQuality, maxQuality: maxQuality) } @@ -401,7 +404,10 @@ public extension GroupCallParticipantsContext.Participant { guard let presentationDescription = self.presentationDescription else { return nil } - return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, peerId: self.peer.id.id._internalGetInt64Value(), endpointId: presentationDescription.endpointId, ssrcGroups: presentationDescription.ssrcGroups.map { group in + guard let peer = self.peer else { + return nil + } + return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, peerId: peer.id.id._internalGetInt64Value(), endpointId: presentationDescription.endpointId, ssrcGroups: presentationDescription.ssrcGroups.map { group in PresentationGroupCallRequestedVideo.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs) }, minQuality: minQuality, maxQuality: maxQuality) } diff --git a/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift b/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift index d87d06bb34..e8187e7734 100644 --- a/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift +++ b/submodules/TelegramCallsUI/Sources/AccountGroupCallContextImpl.swift @@ -57,7 +57,8 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext { id: call.id, reference: .id(id: call.id, accessHash: call.accessHash), state: state, - previousServiceState: nil + previousServiceState: nil, + e2eContext: nil ) self.participantsContext = context diff --git a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift index 04a82610a5..cd20bc2d9a 100644 --- a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift @@ -425,8 +425,8 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { if let members = currentMembers { var speakingPeers: [Peer] = [] for member in members.participants { - if members.speakingParticipants.contains(member.peer.id) { - speakingPeers.append(member.peer) + if let memberPeer = member.peer, members.speakingParticipants.contains(memberPeer.id) { + speakingPeers.append(memberPeer._asPeer()) } } speakingPeer = speakingPeers.first diff --git a/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift index 28c9f40f9a..0fcc407820 100644 --- a/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift +++ b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift @@ -395,7 +395,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { if data.info.isStream { self.avatarsContent = self.avatarsContext.update(peers: [], animated: false) } else { - self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { EnginePeer($0.peer) }, animated: false) + self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.compactMap { $0.peer }, animated: false) if let imageDisposable = self.imageDisposable { self.imageDisposable = nil @@ -430,7 +430,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { if let info = summaryState.info, info.isStream { strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: [], animated: false) } else { - strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { EnginePeer($0.peer) }, animated: false) + strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.compactMap { $0.peer }, animated: false) } if let (size, leftInset, rightInset, isHidden) = strongSelf.validLayout { @@ -513,7 +513,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { if data.info.isStream { self.avatarsContent = self.avatarsContext.update(peers: [], animated: false) } else { - self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { EnginePeer($0.peer) }, animated: false) + self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.compactMap { $0.peer }, animated: false) } updateAudioLevels = true diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index a52f428d9b..fbf1b0646a 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -908,7 +908,7 @@ public final class PresentationCallImpl: PresentationCall { var found = false if let members { for participant in members.participants { - if participant.peer.id == waitForRemotePeerId { + if participant.id == .peer(waitForRemotePeerId) { found = true break } @@ -921,7 +921,7 @@ public final class PresentationCallImpl: PresentationCall { if waitForLocalVideo { if let members { for participant in members.participants { - if participant.peer.id == state.myPeerId { + if participant.id == .peer(state.myPeerId) { if participant.videoDescription == nil { return false } @@ -932,7 +932,7 @@ public final class PresentationCallImpl: PresentationCall { if let waitForRemoteVideo { if let members { for participant in members.participants { - if participant.peer.id == waitForRemoteVideo { + if participant.id == .peer(waitForRemoteVideo) { if participant.videoDescription == nil { return false } diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index b46482df4e..561a94c07f 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -331,9 +331,13 @@ private final class ConferenceCallE2EContextStateImpl: ConferenceCallE2EContextS func getEmojiState() -> Data? { return self.call.emojiState() } + + func getParticipants() -> [ConferenceCallE2EContext.BlockchainParticipant] { + return self.call.participants().map { ConferenceCallE2EContext.BlockchainParticipant(userId: $0.userId, internalId: $0.internalId) } + } func getParticipantIds() -> [Int64] { - return self.call.participantIds().compactMap { $0.int64Value } + return self.call.participants().map { $0.userId } } func applyBlock(block: Data) { @@ -1246,7 +1250,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } if let sourceContext = sourceContext, let initialState = sourceContext.immediateState { - let temporaryParticipantsContext = self.accountContext.engine.calls.groupCall(peerId: self.peerId, myPeerId: myPeerId, id: sourceContext.id, reference: sourceContext.reference, state: initialState, previousServiceState: sourceContext.serviceState) + let temporaryParticipantsContext = self.accountContext.engine.calls.groupCall(peerId: self.peerId, myPeerId: myPeerId, id: sourceContext.id, reference: sourceContext.reference, state: initialState, previousServiceState: sourceContext.serviceState, e2eContext: self.e2eContext) self.temporaryParticipantsContext = temporaryParticipantsContext self.participantsContextStateDisposable.set((combineLatest(queue: .mainQueue(), myPeerData, @@ -1274,14 +1278,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if oldMyPeerId != myPeerId { for i in 0 ..< participants.count { - if participants[i].peer.id == oldMyPeerId { + if participants[i].id == .peer(oldMyPeerId) { participants.remove(at: i) break } } } - if !participants.contains(where: { $0.peer.id == myPeerId }) { + if !participants.contains(where: { $0.id == .peer(myPeerId) }) { if let (myPeer, aboutText) = myPeerData { let about: String? if let aboutText = aboutText { @@ -1290,7 +1294,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { about = " " } participants.append(GroupCallParticipantsContext.Participant( - peer: myPeer._asPeer(), + id: .peer(myPeer.id), + peer: myPeer, ssrc: nil, videoDescription: nil, presentationDescription: nil, @@ -1315,7 +1320,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { topParticipants.append(participant) } - if let index = updatedInvitedPeers.firstIndex(where: { $0.id == participant.peer.id }) { + if let index = updatedInvitedPeers.firstIndex(where: { participant.id == .peer($0.id) }) { updatedInvitedPeers.remove(at: index) didUpdateInvitedPeers = true } @@ -1370,7 +1375,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { about = " " } participants.append(GroupCallParticipantsContext.Participant( - peer: myPeer._asPeer(), + id: .peer(myPeer.id), + peer: myPeer, ssrc: nil, videoDescription: nil, presentationDescription: nil, @@ -1495,7 +1501,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { isStream: callInfo.isStream, version: 0 ), - previousServiceState: nil + previousServiceState: nil, + e2eContext: self.e2eContext ) self.temporaryParticipantsContext = nil self.participantsContext = participantsContext @@ -1555,7 +1562,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { about = " " } participants.append(GroupCallParticipantsContext.Participant( - peer: myPeer._asPeer(), + id: .peer(myPeer.id), + peer: myPeer, ssrc: nil, videoDescription: nil, presentationDescription: nil, @@ -1967,11 +1975,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.ssrcMapping.removeAll() for participant in joinCallResult.state.participants { - if let ssrc = participant.ssrc { - self.ssrcMapping[ssrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: false) + if let ssrc = participant.ssrc, let participantPeer = participant.peer { + self.ssrcMapping[ssrc] = SsrcMapping(peerId: participantPeer.id, isPresentation: false) } - if let presentationSsrc = participant.presentationDescription?.audioSsrc { - self.ssrcMapping[presentationSsrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: true) + if let presentationSsrc = participant.presentationDescription?.audioSsrc, let participantPeer = participant.peer { + self.ssrcMapping[presentationSsrc] = SsrcMapping(peerId: participantPeer.id, isPresentation: true) } } @@ -2268,7 +2276,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { id: callInfo.id, reference: reference, state: initialState, - previousServiceState: serviceState + previousServiceState: serviceState, + e2eContext: self.e2eContext ) self.temporaryParticipantsContext = nil self.participantsContext = participantsContext @@ -2367,14 +2376,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if let (ignorePeerId, ignoreSsrc) = self.ignorePreviousJoinAsPeerId { for i in 0 ..< participants.count { - if participants[i].peer.id == ignorePeerId && participants[i].ssrc == ignoreSsrc { + if participants[i].id == .peer(ignorePeerId) && participants[i].ssrc == ignoreSsrc { participants.remove(at: i) break } } } - if !participants.contains(where: { $0.peer.id == myPeerId }) && !self.leaving { + if !participants.contains(where: { $0.id == .peer(myPeerId) }) && !self.leaving { if let (myPeer, cachedData) = myPeerAndCachedData { let about: String? if let cachedData = cachedData as? CachedUserData { @@ -2386,7 +2395,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } participants.append(GroupCallParticipantsContext.Participant( - peer: myPeer, + id: .peer(myPeer.id), + peer: EnginePeer(myPeer), ssrc: nil, videoDescription: nil, presentationDescription: nil, @@ -2415,13 +2425,17 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } if let ssrc = participant.ssrc { - self.ssrcMapping[ssrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: false) + if let participantPeer = participant.peer { + self.ssrcMapping[ssrc] = SsrcMapping(peerId: participantPeer.id, isPresentation: false) + } } if let presentationSsrc = participant.presentationDescription?.audioSsrc { - self.ssrcMapping[presentationSsrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: true) + if let participantPeer = participant.peer { + self.ssrcMapping[presentationSsrc] = SsrcMapping(peerId: participantPeer.id, isPresentation: true) + } } - if participant.peer.id == self.joinAsPeerId { + if participant.id == .peer(self.joinAsPeerId) { if let (myPeer, cachedData) = myPeerAndCachedData { let about: String? if let cachedData = cachedData as? CachedUserData { @@ -2431,7 +2445,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } else { about = " " } - participant.peer = myPeer + participant.peer = EnginePeer(myPeer) participant.about = about } @@ -2531,7 +2545,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } - if let index = updatedInvitedPeers.firstIndex(where: { $0.id == participant.peer.id }) { + if let index = updatedInvitedPeers.firstIndex(where: { participant.id == .peer($0.id) }) { updatedInvitedPeers.remove(at: index) didUpdateInvitedPeers = true } @@ -2646,24 +2660,28 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if remainingSsrcs.contains(audioSsrc) { remainingSsrcs.remove(audioSsrc) - result.append(OngoingGroupCallContext.MediaChannelDescription( - kind: .audio, - peerId: participant.peer.id.id._internalGetInt64Value(), - audioSsrc: audioSsrc, - videoDescription: nil - )) + if let participantPeer = participant.peer { + result.append(OngoingGroupCallContext.MediaChannelDescription( + kind: .audio, + peerId: participantPeer.id.id._internalGetInt64Value(), + audioSsrc: audioSsrc, + videoDescription: nil + )) + } } if let screencastSsrc = participant.presentationDescription?.audioSsrc { if remainingSsrcs.contains(screencastSsrc) { remainingSsrcs.remove(screencastSsrc) - result.append(OngoingGroupCallContext.MediaChannelDescription( - kind: .audio, - peerId: participant.peer.id.id._internalGetInt64Value(), - audioSsrc: screencastSsrc, - videoDescription: nil - )) + if let participantPeer = participant.peer { + result.append(OngoingGroupCallContext.MediaChannelDescription( + kind: .audio, + peerId: participantPeer.id.id._internalGetInt64Value(), + audioSsrc: screencastSsrc, + videoDescription: nil + )) + } } } } @@ -2849,7 +2867,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if let participantsContext = self.participantsContext, let immediateState = participantsContext.immediateState { for participant in immediateState.participants { - if participant.peer.id == previousPeerId { + if participant.id == .peer(previousPeerId) { self.temporaryJoinTimestamp = participant.joinTimestamp self.temporaryActivityTimestamp = participant.activityTimestamp self.temporaryActivityRank = participant.activityRank @@ -3008,7 +3026,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return } for participant in membersValue.participants { - if participant.peer.id == self.joinAsPeerId { + if participant.id == .peer(self.joinAsPeerId) { if participant.hasRaiseHand { return } @@ -3024,7 +3042,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return } for participant in membersValue.participants { - if participant.peer.id == self.joinAsPeerId { + if participant.id == .peer(self.joinAsPeerId) { if !participant.hasRaiseHand { return } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift index 88379221c0..8729fa24a4 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift @@ -143,7 +143,9 @@ final class VideoChatParticipantThumbnailComponent: Component { gesture.cancel() return } - component.contextAction?(EnginePeer(component.participant.peer), self.extractedContainerView, gesture) + if let participantPeer = component.participant.peer { + component.contextAction?(participantPeer, self.extractedContainerView, gesture) + } } } @@ -213,10 +215,10 @@ final class VideoChatParticipantThumbnailComponent: Component { let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) * 0.5), y: 7.0), size: avatarSize) transition.setFrame(view: avatarNode.view, frame: avatarFrame) avatarNode.updateSize(size: avatarSize) - if component.participant.peer.smallProfileImage != nil { - avatarNode.setPeerV2(context: component.call.accountContext, theme: component.theme, peer: EnginePeer(component.participant.peer), displayDimensions: avatarSize) + if component.participant.peer?.smallProfileImage != nil { + avatarNode.setPeerV2(context: component.call.accountContext, theme: component.theme, peer: component.participant.peer, displayDimensions: avatarSize) } else { - avatarNode.setPeer(context: component.call.accountContext, theme: component.theme, peer: EnginePeer(component.participant.peer), displayDimensions: avatarSize) + avatarNode.setPeer(context: component.call.accountContext, theme: component.theme, peer: component.participant.peer, displayDimensions: avatarSize) } let muteStatusSize = self.muteStatus.update( @@ -241,7 +243,7 @@ final class VideoChatParticipantThumbnailComponent: Component { let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: EnginePeer(component.participant.peer).compactDisplayTitle, font: Font.semibold(13.0), textColor: .white)) + text: .plain(NSAttributedString(string: component.participant.peer?.compactDisplayTitle ?? "User \(component.participant.id)", font: Font.semibold(13.0), textColor: .white)) )), environment: {}, containerSize: CGSize(width: availableSize.width - 6.0 * 2.0 - 12.0, height: 100.0) @@ -436,10 +438,10 @@ final class VideoChatParticipantThumbnailComponent: Component { final class VideoChatExpandedParticipantThumbnailsComponent: Component { final class Participant: Equatable { struct Key: Hashable { - var id: EnginePeer.Id + var id: GroupCallParticipantsContext.Participant.Id var isPresentation: Bool - init(id: EnginePeer.Id, isPresentation: Bool) { + init(id: GroupCallParticipantsContext.Participant.Id, isPresentation: Bool) { self.id = id self.isPresentation = isPresentation } @@ -449,7 +451,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { let isPresentation: Bool var key: Key { - return Key(id: self.participant.peer.id, isPresentation: self.isPresentation) + return Key(id: self.participant.id, isPresentation: self.isPresentation) } init( @@ -657,6 +659,12 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { let itemFrame = itemLayout.frame(at: i) let participantKey = participant.key + + var isSpeaking = false + if let participantPeer = participant.participant.peer { + isSpeaking = component.speakingParticipants.contains(participantPeer.id) + } + let _ = itemView.view.update( transition: itemTransition, component: AnyComponent(VideoChatParticipantThumbnailComponent( @@ -665,7 +673,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component { participant: participant.participant, isPresentation: participant.isPresentation, isSelected: component.selectedParticipant == participant.key, - isSpeaking: component.speakingParticipants.contains(participant.participant.peer.id), + isSpeaking: isSpeaking, displayVideo: component.displayVideo, interfaceOrientation: component.interfaceOrientation, action: { [weak self] in diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift index 2eede132d0..cab65d118b 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift @@ -133,14 +133,14 @@ private final class BlobView: UIView { final class VideoChatParticipantAvatarComponent: Component { let call: VideoChatCall - let peer: EnginePeer + let peer: EnginePeer? let myPeerId: EnginePeer.Id let isSpeaking: Bool let theme: PresentationTheme init( call: VideoChatCall, - peer: EnginePeer, + peer: EnginePeer?, myPeerId: EnginePeer.Id, isSpeaking: Bool, theme: PresentationTheme @@ -267,7 +267,7 @@ final class VideoChatParticipantAvatarComponent: Component { tintTransition.setTintColor(layer: blobView.blobsLayer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor) } - if component.peer.smallProfileImage != nil { + if component.peer?.smallProfileImage != nil { avatarNode.setPeerV2( context: component.call.accountContext, theme: component.theme, @@ -296,7 +296,7 @@ final class VideoChatParticipantAvatarComponent: Component { var isSpeaking: Bool } - let peerId = component.peer.id + let peerId = component.peer?.id let levelSignal: Signal if peerId == component.myPeerId { levelSignal = component.call.myAudioLevelAndSpeaking diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index 70ed81c1ac..25835f0790 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -245,7 +245,9 @@ final class VideoChatParticipantVideoComponent: Component { gesture.cancel() return } - component.contextAction?(EnginePeer(component.participant.peer), self.extractedContainerView, gesture) + if let participantPeer = component.participant.peer { + component.contextAction?(participantPeer, self.extractedContainerView, gesture) + } } } @@ -316,12 +318,12 @@ final class VideoChatParticipantVideoComponent: Component { let controlsAlpha: CGFloat = component.isUIHidden ? 0.0 : 1.0 if previousComponent == nil { - let colors = calculateAvatarColors(context: component.call.accountContext, explicitColorIndex: nil, peerId: component.participant.peer.id, nameColor: component.participant.peer.nameColor, icon: .none, theme: component.theme) + let colors = calculateAvatarColors(context: component.call.accountContext, explicitColorIndex: nil, peerId: component.participant.peer?.id, nameColor: component.participant.peer?.nameColor, icon: .none, theme: component.theme) self.backgroundGradientView.image = generateGradientImage(size: CGSize(width: 8.0, height: 32.0), colors: colors.reversed(), locations: [0.0, 1.0], direction: .vertical) } - if let smallProfileImage = component.participant.peer.smallProfileImage { + if let smallProfileImage = component.participant.peer?.smallProfileImage { let blurredAvatarView: UIImageView if let current = self.blurredAvatarView { blurredAvatarView = current @@ -338,8 +340,8 @@ final class VideoChatParticipantVideoComponent: Component { if self.blurredAvatarDisposable == nil { //TODO:release synchronous - if let imageCache = component.call.accountContext.imageCache as? DirectMediaImageCache, let peerReference = PeerReference(component.participant.peer) { - if let result = imageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: component.participant.peer.profileImageRepresentations.first?.immediateThumbnailData, size: 64, synchronous: false) { + if let participantPeer = component.participant.peer, let imageCache = component.call.accountContext.imageCache as? DirectMediaImageCache, let peerReference = PeerReference(participantPeer._asPeer()) { + if let result = imageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: participantPeer.profileImageRepresentations.first?.immediateThumbnailData, size: 64, synchronous: false) { if let image = result.image { blurredAvatarView.image = blurredAvatarImage(image) } @@ -402,7 +404,7 @@ final class VideoChatParticipantVideoComponent: Component { let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.participant.peer.debugDisplayTitle, font: Font.semibold(16.0), textColor: .white)), + text: .plain(NSAttributedString(string: component.participant.peer?.debugDisplayTitle ?? "User \(component.participant.id)", font: Font.semibold(16.0), textColor: .white)), insets: titleInnerInsets, textShadowColor: UIColor(white: 0.0, alpha: 0.7), textShadowBlur: 8.0 diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index a354a1c543..5fd4a9d86c 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -92,10 +92,10 @@ final class VideoChatParticipantsComponent: Component { } struct VideoParticipantKey: Hashable { - var id: EnginePeer.Id + var id: GroupCallParticipantsContext.Participant.Id var isPresentation: Bool - init(id: EnginePeer.Id, isPresentation: Bool) { + init(id: GroupCallParticipantsContext.Participant.Id, isPresentation: Bool) { self.id = id self.isPresentation = isPresentation } @@ -608,7 +608,7 @@ final class VideoChatParticipantsComponent: Component { let isPresentation: Bool var key: VideoParticipantKey { - return VideoParticipantKey(id: self.participant.peer.id, isPresentation: self.isPresentation) + return VideoParticipantKey(id: self.participant.id, isPresentation: self.isPresentation) } init(participant: GroupCallParticipantsContext.Participant, isPresentation: Bool) { @@ -673,7 +673,7 @@ final class VideoChatParticipantsComponent: Component { private var expandedThumbnailsView: ComponentView? private var expandedSpeakingToast: ComponentView? - private var listItemViews: [EnginePeer.Id: ListItem] = [:] + private var listItemViews: [GroupCallParticipantsContext.Participant.Id: ListItem] = [:] private let listItemViewContainer: UIView private let listItemViewSeparatorContainer: SimpleLayer private let listItemsBackground = ComponentView() @@ -959,7 +959,7 @@ final class VideoChatParticipantsComponent: Component { } } - var visibleParticipants: [EnginePeer.Id] = [] + var visibleParticipants: [GroupCallParticipantsContext.Participant.Id] = [] for index in validGridItemIndices { let videoParticipant = self.gridParticipants[index] @@ -988,7 +988,7 @@ final class VideoChatParticipantsComponent: Component { } if isItemExpanded || (index >= clippedVisibleGridItemRange.minIndex && index <= clippedVisibleGridItemRange.maxIndex) { - visibleParticipants.append(videoParticipant.key.id) + visibleParticipants.append(videoParticipant.participant.id) } var suppressItemExpansionCollapseAnimation = false @@ -1048,6 +1048,11 @@ final class VideoChatParticipantsComponent: Component { } else { itemAlpha = 1.0 } + + var isSpeaking = false + if let participantPeer = videoParticipant.participant.peer { + isSpeaking = component.speakingParticipants.contains(participantPeer.id) + } let _ = itemView.view.update( transition: itemTransition, @@ -1056,9 +1061,9 @@ final class VideoChatParticipantsComponent: Component { strings: component.strings, call: component.call, participant: videoParticipant.participant, - isMyPeer: videoParticipant.participant.peer.id == component.participants?.myPeerId, + isMyPeer: videoParticipant.participant.peer?.id == component.participants?.myPeerId, isPresentation: videoParticipant.isPresentation, - isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id), + isSpeaking: isSpeaking, maxVideoQuality: component.maxVideoQuality, isExpanded: isItemExpanded, isUIHidden: isItemUIHidden || self.isPinchToZoomActive, @@ -1205,23 +1210,23 @@ final class VideoChatParticipantsComponent: Component { self.gridItemViews.removeValue(forKey: itemId) } - var validListItemIds: [EnginePeer.Id] = [] + var validListItemIds: [GroupCallParticipantsContext.Participant.Id] = [] let visibleListItemRange = itemLayout.visibleListItemRange(for: self.scrollView.bounds) let clippedVisibleListItemRange = itemLayout.visibleListItemRange(for: clippedScrollViewBounds) if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex { for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex { let itemFrame = itemLayout.listItemFrame(at: i) - let participantPeerId: EnginePeer.Id + let participantItemId: GroupCallParticipantsContext.Participant.Id let peerItemComponent: PeerListItemComponent if i < self.listParticipants.count { let participant = self.listParticipants[i] - participantPeerId = participant.peer.id + participantItemId = participant.id let subtitle: PeerListItemComponent.Subtitle - if participant.peer.id == component.call.accountContext.account.peerId { + if participant.id == .peer(component.call.accountContext.account.peerId) { subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_You, color: .accent) - } else if component.speakingParticipants.contains(participant.peer.id) { + } else if let participantPeer = participant.peer, component.speakingParticipants.contains(participantPeer.id) { if let volume = participant.volume, volume / 100 != 100 { subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusSpeakingVolume("\(volume / 100)%").string, color: .constructive) } else { @@ -1233,10 +1238,14 @@ final class VideoChatParticipantsComponent: Component { subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusListening, color: .neutral) } + var isSpeaking = false + if let participantPeer = participant.peer { + isSpeaking = component.speakingParticipants.contains(participantPeer.id) + } let rightAccessoryComponent: AnyComponent = AnyComponent(VideoChatParticipantStatusComponent( muteState: participant.muteState, hasRaiseHand: participant.hasRaiseHand, - isSpeaking: component.speakingParticipants.contains(participant.peer.id), + isSpeaking: isSpeaking, theme: component.theme )) @@ -1246,15 +1255,15 @@ final class VideoChatParticipantsComponent: Component { strings: component.strings, style: .generic, sideInset: 0.0, - title: EnginePeer(participant.peer).displayTitle(strings: component.strings, displayOrder: .firstLast), + title: participant.peer?.displayTitle(strings: component.strings, displayOrder: .firstLast) ?? "User \(participant.id)", avatarComponent: AnyComponent(VideoChatParticipantAvatarComponent( call: component.call, - peer: EnginePeer(participant.peer), + peer: participant.peer, myPeerId: component.participants?.myPeerId ?? component.call.accountContext.account.peerId, - isSpeaking: component.speakingParticipants.contains(participant.peer.id), + isSpeaking: isSpeaking, theme: component.theme )), - peer: EnginePeer(participant.peer), + peer: participant.peer, subtitle: subtitle, subtitleAccessory: .none, presence: nil, @@ -1280,7 +1289,7 @@ final class VideoChatParticipantsComponent: Component { ) } else { let invitedPeer = component.invitedPeers[i - self.listParticipants.count] - participantPeerId = invitedPeer.peer.id + participantItemId = .peer(invitedPeer.peer.id) let subtitle: PeerListItemComponent.Subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusInvited, color: .neutral) @@ -1328,20 +1337,20 @@ final class VideoChatParticipantsComponent: Component { ) } - validListItemIds.append(participantPeerId) + validListItemIds.append(participantItemId) if i >= clippedVisibleListItemRange.minIndex && i <= clippedVisibleListItemRange.maxIndex { - visibleParticipants.append(participantPeerId) + visibleParticipants.append(participantItemId) } var itemTransition = transition let itemView: ListItem - if let current = self.listItemViews[participantPeerId] { + if let current = self.listItemViews[participantItemId] { itemView = current } else { itemTransition = itemTransition.withAnimation(.none) itemView = ListItem() - self.listItemViews[participantPeerId] = itemView + self.listItemViews[participantItemId] = itemView } let _ = itemView.view.update( @@ -1375,7 +1384,7 @@ final class VideoChatParticipantsComponent: Component { } } - var removedListItemIds: [EnginePeer.Id] = [] + var removedListItemIds: [GroupCallParticipantsContext.Participant.Id] = [] for (itemId, itemView) in self.listItemViews { if !validListItemIds.contains(itemId) { removedListItemIds.append(itemId) @@ -1512,7 +1521,7 @@ final class VideoChatParticipantsComponent: Component { expandedThumbnailsComponentView.alpha = expandedThumbnailsAlpha let fromReferenceFrame: CGRect - if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) { + if let index = self.gridParticipants.firstIndex(where: { $0.participant.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) { fromReferenceFrame = self.gridItemViewContainer.convert(itemLayout.gridItemFrame(at: index), to: self.expandedGridItemContainer) } else { fromReferenceFrame = previousExpandedGridItemContainerFrame @@ -1571,7 +1580,7 @@ final class VideoChatParticipantsComponent: Component { expandedControlsComponentView.alpha = expandedControlsAlpha let fromReferenceFrame: CGRect - if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) { + if let index = self.gridParticipants.firstIndex(where: { $0.participant.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) { fromReferenceFrame = self.gridItemViewContainer.convert(itemLayout.gridItemFrame(at: index), to: self.expandedGridItemContainer) } else { fromReferenceFrame = previousExpandedGridItemContainerFrame @@ -1591,7 +1600,7 @@ final class VideoChatParticipantsComponent: Component { self.expandedThumbnailsView = nil if transition.containedViewLayoutTransition.isAnimated, let expandedThumbnailsComponentView = expandedThumbnailsView.view { - if let collapsingItemView = self.gridItemViews.values.first(where: { $0.isCollapsing }), let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == collapsingItemView.key.id && $0.isPresentation == collapsingItemView.key.isPresentation }) { + if let collapsingItemView = self.gridItemViews.values.first(where: { $0.isCollapsing }), let index = self.gridParticipants.firstIndex(where: { $0.participant.id == collapsingItemView.key.id && $0.isPresentation == collapsingItemView.key.isPresentation }) { let targetLocalItemFrame = itemLayout.gridItemFrame(at: index) var targetItemFrame = self.gridItemViewContainer.convert(targetLocalItemFrame, to: self) targetItemFrame.origin.y -= expandedGridItemContainerFrame.minY @@ -1612,7 +1621,7 @@ final class VideoChatParticipantsComponent: Component { self.expandedControlsView = nil if transition.containedViewLayoutTransition.isAnimated, let expandedControlsComponentView = expandedControlsView.view { - if let collapsingItemView = self.gridItemViews.values.first(where: { $0.isCollapsing }), let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == collapsingItemView.key.id && $0.isPresentation == collapsingItemView.key.isPresentation }) { + if let collapsingItemView = self.gridItemViews.values.first(where: { $0.isCollapsing }), let index = self.gridParticipants.firstIndex(where: { $0.participant.id == collapsingItemView.key.id && $0.isPresentation == collapsingItemView.key.isPresentation }) { let targetLocalItemFrame = itemLayout.gridItemFrame(at: index) var targetItemFrame = self.gridItemViewContainer.convert(targetLocalItemFrame, to: self) targetItemFrame.origin.y -= expandedGridItemContainerFrame.minY @@ -1630,7 +1639,7 @@ final class VideoChatParticipantsComponent: Component { } } - if let expandedVideoState = component.expandedVideoState, expandedVideoState.isMainParticipantPinned, let participants = component.participants, !component.speakingParticipants.isEmpty, let firstOther = component.speakingParticipants.first(where: { $0 != expandedVideoState.mainParticipant.id }), let speakingPeer = participants.participants.first(where: { $0.peer.id == firstOther })?.peer { + if let expandedVideoState = component.expandedVideoState, expandedVideoState.isMainParticipantPinned, let participants = component.participants, !component.speakingParticipants.isEmpty, let firstOther = component.speakingParticipants.first(where: { expandedVideoState.mainParticipant.id != .peer($0) }), let speakingPeer = participants.participants.first(where: { $0.id == .peer(firstOther) })?.peer { let expandedSpeakingToast: ComponentView var expandedSpeakingToastTransition = transition if let current = self.expandedSpeakingToast { @@ -1644,21 +1653,21 @@ final class VideoChatParticipantsComponent: Component { transition: expandedSpeakingToastTransition, component: AnyComponent(VideoChatExpandedSpeakingToastComponent( context: component.call.accountContext, - peer: EnginePeer(speakingPeer), + peer: speakingPeer, strings: component.strings, theme: component.theme, action: { [weak self] peer in guard let self, let component = self.component, let participants = component.participants else { return } - guard let participant = participants.participants.first(where: { $0.peer.id == peer.id }) else { + guard let participant = participants.participants.first(where: { $0.id == .peer(peer.id) }) else { return } var key: VideoParticipantKey? if participant.presentationDescription != nil { - key = VideoParticipantKey(id: peer.id, isPresentation: true) + key = VideoParticipantKey(id: .peer(peer.id), isPresentation: true) } else if participant.videoDescription != nil { - key = VideoParticipantKey(id: peer.id, isPresentation: false) + key = VideoParticipantKey(id: .peer(peer.id), isPresentation: false) } if let key { component.updateMainParticipant(key, nil) @@ -1701,7 +1710,13 @@ final class VideoChatParticipantsComponent: Component { } } - component.visibleParticipantsUpdated(Set(visibleParticipants)) + component.visibleParticipantsUpdated(Set(visibleParticipants.compactMap { + if case let .peer(id) = $0 { + return id + } else { + return nil + } + })) } func setEventCycleState(scrollView: UIScrollView, eventCycleState: EventCycleState?) { @@ -1712,7 +1727,7 @@ final class VideoChatParticipantsComponent: Component { } } - func itemFrame(peerId: EnginePeer.Id, isPresentation: Bool) -> CGRect? { + func itemFrame(peerId: GroupCallParticipantsContext.Participant.Id, isPresentation: Bool) -> CGRect? { for (key, itemView) in self.gridItemViews { if key.id == peerId && key.isPresentation == isPresentation { if let itemComponentView = itemView.view.view { @@ -1723,7 +1738,7 @@ final class VideoChatParticipantsComponent: Component { return nil } - func updateItemPlaceholder(peerId: EnginePeer.Id, isPresentation: Bool, placeholder: VideoSource.Output) { + func updateItemPlaceholder(peerId: GroupCallParticipantsContext.Participant.Id, isPresentation: Bool, placeholder: VideoSource.Output) { for (key, itemView) in self.gridItemViews { if key.id == peerId && key.isPresentation == isPresentation { if let itemComponentView = itemView.view.view as? VideoChatParticipantVideoComponent.View { @@ -1781,7 +1796,7 @@ final class VideoChatParticipantsComponent: Component { if participant.videoDescription != nil { hasVideo = true let videoParticipant = VideoParticipant(participant: participant, isPresentation: false) - if participant.peer.id == participants.myPeerId { + if participant.id == .peer(participants.myPeerId) { gridParticipants.insert(videoParticipant, at: 0) } else { gridParticipants.append(videoParticipant) @@ -1790,14 +1805,14 @@ final class VideoChatParticipantsComponent: Component { if participant.presentationDescription != nil { hasVideo = true let videoParticipant = VideoParticipant(participant: participant, isPresentation: true) - if participant.peer.id == participants.myPeerId { + if participant.id == .peer(participants.myPeerId) { gridParticipants.insert(videoParticipant, at: 0) } else { gridParticipants.append(videoParticipant) } } if !hasVideo || component.layout.videoColumn != nil { - if participant.peer.id == participants.myPeerId && !isFullyMuted { + if participant.id == .peer(participants.myPeerId) && !isFullyMuted { listParticipants.insert(participant, at: 0) } else { listParticipants.append(participant) @@ -1927,7 +1942,7 @@ final class VideoChatParticipantsComponent: Component { for participant in participants.participants { var maxVideoQuality: PresentationGroupCallRequestedVideo.Quality = .medium if let expandedVideoState = component.expandedVideoState { - if expandedVideoState.mainParticipant.id == participant.peer.id, !expandedVideoState.mainParticipant.isPresentation { + if expandedVideoState.mainParticipant.id == participant.id, !expandedVideoState.mainParticipant.isPresentation { if component.maxVideoQuality == Int.max { maxVideoQuality = .full } else if component.maxVideoQuality == 360 { @@ -1942,7 +1957,7 @@ final class VideoChatParticipantsComponent: Component { var maxPresentationQuality: PresentationGroupCallRequestedVideo.Quality = .medium if let expandedVideoState = component.expandedVideoState { - if expandedVideoState.mainParticipant.id == participant.peer.id, expandedVideoState.mainParticipant.isPresentation { + if expandedVideoState.mainParticipant.id == participant.id, expandedVideoState.mainParticipant.isPresentation { if component.maxVideoQuality == Int.max { maxVideoQuality = .full } else if component.maxVideoQuality == 360 { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 249521f415..f15d3cc12f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -343,17 +343,17 @@ final class VideoChatScreenComponent: Component { sourceCallControllerView?.removeFromSuperview() } - var expandedPeer: (id: EnginePeer.Id, isPresentation: Bool)? + var expandedPeer: (id: GroupCallParticipantsContext.Participant.Id, isPresentation: Bool)? if let animateOutData, animateOutData.incomingVideoLayer != nil, let members = self.members { - if let participant = members.participants.first(where: { $0.peer.id == animateOutData.incomingPeerId }) { + if let participant = members.participants.first(where: { $0.id == .peer(animateOutData.incomingPeerId) }) { if let _ = participant.videoDescription { - expandedPeer = (participant.peer.id, false) - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: true) + expandedPeer = (participant.id, false) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: true) } - } else if let participant = members.participants.first(where: { $0.peer.id == sourceCallController.call.context.account.peerId }) { + } else if let participant = members.participants.first(where: { $0.id == .peer(sourceCallController.call.context.account.peerId) }) { if let _ = participant.videoDescription { - expandedPeer = (participant.peer.id, false) - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: true) + expandedPeer = (participant.id, false) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: true) } } } @@ -1164,7 +1164,8 @@ final class VideoChatScreenComponent: Component { } participants.append(GroupCallParticipantsContext.Participant( - peer: myPeer._asPeer(), + id: .peer(myPeer.id), + peer: myPeer, ssrc: nil, videoDescription: myVideoDescription, presentationDescription: nil, @@ -1189,7 +1190,8 @@ final class VideoChatScreenComponent: Component { } participants.append(GroupCallParticipantsContext.Participant( - peer: remotePeer._asPeer(), + id: .peer(remotePeer.id), + peer: remotePeer, ssrc: nil, videoDescription: remoteVideoDescription, presentationDescription: nil, @@ -1235,7 +1237,7 @@ final class VideoChatScreenComponent: Component { self.members = component.initialData.members self.invitedPeers = component.initialData.invitedPeers if let members = self.members { - self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.peer.id }) }) + self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.id == .peer(invitedPeer.peer.id) }) }) } self.callState = component.initialData.callState } @@ -1276,7 +1278,7 @@ final class VideoChatScreenComponent: Component { self.members = members if let members { - self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.peer.id }) }) + self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.id == .peer(invitedPeer.peer.id) }) }) } if let members, let expandedParticipantsVideoState = self.expandedParticipantsVideoState, !expandedParticipantsVideoState.isUIHidden { @@ -1299,28 +1301,28 @@ final class VideoChatScreenComponent: Component { if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, let members { if CFAbsoluteTimeGetCurrent() > self.focusedSpeakerAutoSwitchDeadline, !expandedParticipantsVideoState.isMainParticipantPinned, let participant = members.participants.first(where: { participant in - if let callState = self.callState, participant.peer.id == callState.myPeerId { + if let callState = self.callState, participant.id == .peer(callState.myPeerId) { return false } if participant.videoDescription != nil || participant.presentationDescription != nil { - if members.speakingParticipants.contains(participant.peer.id) { + if let participantPeer = participant.peer, members.speakingParticipants.contains(participantPeer.id) { return true } } return false }) { - if participant.peer.id != expandedParticipantsVideoState.mainParticipant.id { + if participant.id != expandedParticipantsVideoState.mainParticipant.id { if participant.presentationDescription != nil { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } else { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 1.0 } } if let _ = members.participants.first(where: { participant in - if participant.peer.id == expandedParticipantsVideoState.mainParticipant.id { + if participant.id == expandedParticipantsVideoState.mainParticipant.id { if expandedParticipantsVideoState.mainParticipant.isPresentation { if participant.presentationDescription == nil { return false @@ -1344,9 +1346,9 @@ final class VideoChatScreenComponent: Component { return false }) { if participant.presentationDescription != nil { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } else { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 1.0 } else { @@ -1365,8 +1367,8 @@ final class VideoChatScreenComponent: Component { var speakingParticipantPeers: [EnginePeer] = [] if let members, !members.speakingParticipants.isEmpty { for participant in members.participants { - if members.speakingParticipants.contains(participant.peer.id) { - speakingParticipantPeers.append(EnginePeer(participant.peer)) + if let participantPeer = participant.peer, members.speakingParticipants.contains(participantPeer.id) { + speakingParticipantPeers.append(participantPeer) } } } @@ -1401,7 +1403,7 @@ final class VideoChatScreenComponent: Component { var invitedPeers = invitedPeers if let members { - invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.peer.id == invitedPeer.peer.id }) }) + invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.id == .peer(invitedPeer.peer.id) }) }) } if self.invitedPeers != invitedPeers { @@ -1612,28 +1614,28 @@ final class VideoChatScreenComponent: Component { if let expandedParticipantsVideoState = self.expandedParticipantsVideoState { if CFAbsoluteTimeGetCurrent() > self.focusedSpeakerAutoSwitchDeadline, !expandedParticipantsVideoState.isMainParticipantPinned, let participant = members.participants.first(where: { participant in - if let callState = self.callState, participant.peer.id == callState.myPeerId { + if let callState = self.callState, participant.id == .peer(callState.myPeerId) { return false } if participant.videoDescription != nil || participant.presentationDescription != nil { - if members.speakingParticipants.contains(participant.peer.id) { + if let participantPeer = participant.peer, members.speakingParticipants.contains(participantPeer.id) { return true } } return false }) { - if participant.peer.id != expandedParticipantsVideoState.mainParticipant.id { + if participant.id != expandedParticipantsVideoState.mainParticipant.id { if participant.presentationDescription != nil { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } else { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 1.0 } } if let _ = members.participants.first(where: { participant in - if participant.peer.id == expandedParticipantsVideoState.mainParticipant.id { + if participant.id == expandedParticipantsVideoState.mainParticipant.id { if expandedParticipantsVideoState.mainParticipant.isPresentation { if participant.presentationDescription == nil { return false @@ -1657,9 +1659,9 @@ final class VideoChatScreenComponent: Component { return false }) { if participant.presentationDescription != nil { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } else { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 1.0 } else { @@ -1678,8 +1680,8 @@ final class VideoChatScreenComponent: Component { var speakingParticipantPeers: [EnginePeer] = [] if !members.speakingParticipants.isEmpty { for participant in members.participants { - if members.speakingParticipants.contains(participant.peer.id) { - speakingParticipantPeers.append(EnginePeer(participant.peer)) + if let participantPeer = participant.peer, members.speakingParticipants.contains(participantPeer.id) { + speakingParticipantPeers.append(participantPeer) } } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift index 4d97fef81e..c5273563ca 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift @@ -21,8 +21,8 @@ extension VideoChatScreenComponent.View { disablePeerIds.append(groupCall.accountContext.account.peerId) if let members = self.members { for participant in members.participants { - if !disablePeerIds.contains(participant.peer.id) { - disablePeerIds.append(participant.peer.id) + if let participantPeer = participant.peer, !disablePeerIds.contains(participantPeer.id) { + disablePeerIds.append(participantPeer.id) } } } @@ -99,7 +99,7 @@ extension VideoChatScreenComponent.View { var filters: [ChannelMembersSearchFilter] = [] if let members = self.members { - filters.append(.disable(Array(members.participants.map { $0.peer.id }))) + filters.append(.disable(Array(members.participants.compactMap { $0.peer?.id }))) } if case let .channel(groupPeer) = groupPeer { if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift index 9cbef0c378..9542fe76c0 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -18,7 +18,7 @@ extension VideoChatScreenComponent.View { guard let environment = self.environment else { return } - guard let members = self.members, let participant = members.participants.first(where: { $0.peer.id == id }) else { + guard let members = self.members, let participant = members.participants.first(where: { $0.id == .peer(id) }) else { return } guard let currentCall = self.currentCall else { @@ -35,10 +35,13 @@ extension VideoChatScreenComponent.View { return [] } - var items: [ContextMenuItem] = [] + guard let peer = participant.peer else { + return [] + } + var items: [ContextMenuItem] = [] var hasVolumeSlider = false - let peer = participant.peer + if let muteState = muteState, !muteState.canUnmute || muteState.mutedByYou { } else { if callState.canManageCall || callState.myPeerId != id { @@ -65,7 +68,7 @@ extension VideoChatScreenComponent.View { } } - if callState.myPeerId == id && !hasVolumeSlider && ((participant.about?.isEmpty ?? true) || participant.peer.smallProfileImage == nil) { + if callState.myPeerId == id && !hasVolumeSlider && ((participant.about?.isEmpty ?? true) || participant.peer?.smallProfileImage == nil) { items.append(.custom(VoiceChatInfoContextItem(text: environment.strings.VoiceChat_ImproveYourProfileText, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: theme.actionSheet.primaryTextColor) }), true)) @@ -134,7 +137,7 @@ extension VideoChatScreenComponent.View { } }))) - if let peer = peer as? TelegramUser { + if case let .user(peer) = peer { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ChangeName, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ChangeName"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in @@ -184,7 +187,9 @@ extension VideoChatScreenComponent.View { let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: false) f(.default) - self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(EnginePeer(participant.peer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) + if let participantPeer = participant.peer { + self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(participantPeer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true }) + } }))) } else { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in @@ -207,7 +212,6 @@ extension VideoChatScreenComponent.View { guard let self, case let .group(groupCall) = self.currentCall else { return } - let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: false) f(.default) }))) @@ -228,7 +232,7 @@ extension VideoChatScreenComponent.View { 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 { + if case let .channel(peer) = peer, case .broadcast = peer.info { openTitle = environment.strings.VoiceChat_OpenChannel openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels") } else { @@ -256,7 +260,7 @@ extension VideoChatScreenComponent.View { guard let navigationController else { return } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), keepStack: .always, purposefulAction: {}, peekData: nil)) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) } }) @@ -293,7 +297,7 @@ extension VideoChatScreenComponent.View { let nameDisplayOrder = presentationData.nameDisplayOrder if let chatPeer { - items.append(DeleteChatPeerActionSheetItem(context: groupCall.accountContext, peer: EnginePeer(peer), chatPeer: chatPeer, action: .removeFromGroup, strings: environment.strings, nameDisplayOrder: nameDisplayOrder)) + items.append(DeleteChatPeerActionSheetItem(context: groupCall.accountContext, peer: peer, chatPeer: chatPeer, action: .removeFromGroup, strings: environment.strings, nameDisplayOrder: nameDisplayOrder)) } items.append(ActionSheetButtonItem(title: environment.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak self, weak actionSheet] in @@ -312,7 +316,7 @@ extension VideoChatScreenComponent.View { } } - self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(EnginePeer(peer).displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false }) + self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false }) })) actionSheet.setItemGroups([ diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index d3c7626b85..d79cd9dc5a 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -253,6839 +253,6 @@ public protocol VoiceChatController: ViewController { func dismiss(closing: Bool, manual: Bool) } -final class VoiceChatControllerImpl: ViewController, VoiceChatController { - enum DisplayMode { - case modal(isExpanded: Bool, isFilled: Bool) - case fullscreen(controlsHidden: Bool) - } - - fileprivate final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate { - 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] = [:] - - 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 { - let signal: Signal - if let current = self.audioLevels[peerId] { - signal = current.signal() - } else { - let value = ValuePipe() - 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() - 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: EnginePeer(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: EnginePeer(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(value: nil) - - private var configuration: VoiceChatConfiguration? - - private weak var controller: VoiceChatControllerImpl? - 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 let participantsNode: VoiceChatTimerNode - - 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 currentTotalCount: Int32 = 0 - private var currentInvitedPeers: [EnginePeer]? - private var currentSpeakingPeers: Set? - 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 scrollAtTop = true - - 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(nil) - - private var raisedHandDisplayDisposables: [PeerId: Disposable] = [:] - private var displayedRaisedHands = Set() { - didSet { - self.displayedRaisedHandsPromise.set(self.displayedRaisedHands) - } - } - private let displayedRaisedHandsPromise = ValuePromise>(Set()) - - private var requestedVideoSources = Set() - private var requestedVideoChannels: [PresentationGroupCallRequestedVideo] = [] - - private var videoRenderingContext: VideoRenderingContext - private var videoNodes: [String: GroupVideoNode] = [:] - private var wideVideoNodes = Set() - private var videoOrder: [String] = [] - private var readyVideoEndpointIds = Set() - private var readyVideoEndpointIdsPromise = ValuePromise>(Set()) - private var timeoutedEndpointIds = Set() - private var readyVideoDisposables = DisposableDict() - 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: VoiceChatControllerImpl, 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() - self.actionButton.animationsEnabled = sharedContext.energyUsageSettings.fullTranslucency - - 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 - - self.participantsNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) - - 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 - }) - - var displayAsPeers: Signal<[FoundPeer], NoError> = currentAccountPeer - if let callPeerId = call.peerId { - displayAsPeers = displayAsPeers |> then( - combineLatest(currentAccountPeer, context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: callPeerId)) - |> 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 - } - guard let callPeerId = strongSelf.call.peerId else { - return - } - - let groupPeer = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId)) - 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 case let .channel(groupPeer) = groupPeer { - 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 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: 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, isVideo: false) { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } else { - text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, 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 = strongSelf.presentationData.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), EnginePeer(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: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) - |> 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 case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info { - text = strongSelf.presentationData.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), EnginePeer(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 case let .channel(groupPeer) = groupPeer { - 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 { [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 - case .kicked: - text = presentationData.strings.Channel_AddUserKickedError - } - 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, isVideo: false) { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } else { - text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) - } - })) - } else if case let .legacyGroup(groupPeer) = groupPeer { - 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 { [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(EnginePeer(peer).compactDisplayTitle, EnginePeer(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, isVideo: false) { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } else { - text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) - } - })) - } - })]), in: .window(.root)) - } - } - }) - controller.copyInviteLink = { - dismissController?() - - guard let strongSelf = self else { - return - } - guard let callPeerId = strongSelf.call.peerId else { - return - } - - let _ = (strongSelf.context.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: { link in - guard let strongSelf = self else { - return - } - - if let link = link { - UIPasteboard.general.string = link - - strongSelf.presentUndoOverlay(content: .linkCopied(title: nil, 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(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 in - return .complete() - }).start() - } else { - let _ = (strongSelf.context.engine.peers.updatePeerDescription(peerId: peer.id, description: bio) - |> `catch` { _ -> Signal in - return .complete() - }).start() - } - - strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess, timeout: nil, customUndoText: nil), 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(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess, timeout: nil, customUndoText: nil), 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(EnginePeer(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(EnginePeer(peer)), 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 - } - guard let callPeerId = strongSelf.call.peerId else { - return - } - - let _ = (strongSelf.context.account.postbox.loadedPeerWithId(callPeerId) - |> 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: callPeerId, 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(EnginePeer(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(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(presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items |> map { ContextController.Items(content: .list($0)) }, 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.participantsNode) - 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<[EnginePeer], NoError> = self.call.invitedPeers - |> mapToSignal { peers -> Signal<[EnginePeer], NoError> in - let ids = peers.map(\.id) - return context.engine.data.get(EngineDataList( - ids.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - )) - |> map { itemList -> [EnginePeer] in - return itemList.compactMap { $0 } - } - } - - 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]?.get(AppConfiguration.self) ?? .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 totalCount = Int32(max(1, callMembers?.totalCount ?? 0)) - strongSelf.currentTotalCount = totalCount - - let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(totalCount) - 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 callPeerView: Signal - if let peerId = self.call.peerId { - callPeerView = self.context.account.viewTracker.peerView(peerId) |> map(Optional.init) - } else { - callPeerView = .single(nil) - } - - 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(), callPeerView, titleAndRecording).start(next: { [weak self] view, titleAndRecording in - guard let strongSelf = self else { - return - } - - let (title, isRecording) = titleAndRecording - if let view, let peer = peerViewMainPeer(view) { - let isLivestream: Bool - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - isLivestream = true - } else { - isLivestream = false - } - strongSelf.participantsNode.isHidden = !isLivestream || strongSelf.isScheduled - - let hadPeer = strongSelf.peer != nil - strongSelf.peer = peer - strongSelf.currentTitleIsCustom = title != nil - strongSelf.currentTitle = title ?? EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) - - strongSelf.updateTitle(transition: .immediate) - strongSelf.titleNode.isRecording = isRecording - - if strongSelf.isScheduling && !hadPeer { - strongSelf.updateScheduleButtonTitle() - } - } - 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 - } - - var existingOutputs = Set() - var filteredOutputs: [AudioSessionOutput] = [] - for output in state.0 { - if case let .port(port) = output { - if !existingOutputs.contains(port.name) { - existingOutputs.insert(port.name) - filteredOutputs.append(output) - } - } else { - filteredOutputs.append(output) - } - } - - let wasEmpty = strongSelf.audioOutputState == nil - strongSelf.audioOutputState = (filteredOutputs, state.1) - 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() - 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.visibleContentOffsetChanged = { [weak self] offset in - guard let strongSelf = self else { - return - } - var scrollAtTop = false - if case let .known(value) = offset, value < 180.0 { - scrollAtTop = true - } else { - scrollAtTop = false - } - if scrollAtTop != strongSelf.scrollAtTop { - strongSelf.scrollAtTop = scrollAtTop - strongSelf.updateTitle(transition: .immediate) - } - } - - 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 { - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - return - } - let text = strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) - } - })) - - self.reconnectedAsEventsDisposable.set((self.call.reconnectedAsEvents - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let strongSelf = self else { - return - } - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_DisplayAsSuccess(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } else { - text = strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), 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) - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = presentationData.strings.LiveStream_RecordingInProgress - } else { - text = presentationData.strings.VoiceChat_RecordingInProgress - } - strongSelf.controller?.present(TooltipScreen(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, text: .plain(text: text), 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.makeBlurView(input: input, mainView: videoView))) - } - } - } - } - } - - 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)") - } - } - }) - } - - var lastTimestamp = 0.0 - self.call.onMutedSpeechActivityDetected = { [weak self] value in - Queue.mainQueue().async { - guard let self, value else { - return - } - let timestamp = CFAbsoluteTimeGetCurrent() - if lastTimestamp + 1000.0 < timestamp { - lastTimestamp = timestamp - - self.presentUndoOverlay(content: .info(title: nil, text: self.presentationData.strings.VoiceChat_ToastMicrophoneIsMuted, timeout: nil, customUndoText: nil), action: { _ in - return false - }) - } - } - } - } - - 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(presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceView: self.optionsButton.referenceNode.view)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) - controller.presentInGlobalOverlay(contextController) - } - } - - private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> { - guard let myPeerId = self.callState?.myPeerId else { - return .single([]) - } - guard let callPeerId = self.call.peerId 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(callPeerId), 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(EnginePeer(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: EnginePeer(peer.peer), size: avatarSize)), action: { c, _ in - guard let strongSelf = self else { - return - } - c?.setItems(strongSelf.contextMenuDisplayAsItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - 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() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - } - - if canManageCall { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_EditTitle - } else { - text = strongSelf.presentationData.strings.VoiceChat_EditTitle - } - items.append(.action(ContextMenuActionItem(text: text, 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() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - } - } - - 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 { - if #available(iOS 15.0, *) { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MicrophoneModes, textColor: .primary, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.dismissWithoutContent) - AVCaptureDevice.showSystemUserInterface(.microphoneModes) - }))) - } else { - 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) - - Queue.mainQueue().after(0.88) { - strongSelf.hapticFeedback.success() - } - - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_RecordingSaved - } else { - text = strongSelf.presentationData.strings.VideoChat_RecordingSaved - } - strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: text), 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 { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer else { - return - } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) - }) - } - }) - - return true - } - return false - }) - } - })]) - self?.controller?.present(alertController, in: .window(.root)) - }), false)) - } else { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_StartRecording - } else { - text = strongSelf.presentationData.strings.VoiceChat_StartRecording - } - if strongSelf.callState?.scheduleTimestamp == nil { - items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in - return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.dismissWithoutContent) - - guard let strongSelf = self, let peer = strongSelf.peer else { - return - } - - let controller = VoiceChatRecordingSetupController(context: strongSelf.context, peer: EnginePeer(peer), completion: { [weak self] videoOrientation in - if let strongSelf = self { - let title: String - let text: String - let placeholder: String - if let _ = videoOrientation { - placeholder = strongSelf.presentationData.strings.VoiceChat_RecordingTitlePlaceholderVideo - } else { - placeholder = strongSelf.presentationData.strings.VoiceChat_RecordingTitlePlaceholder - } - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - title = strongSelf.presentationData.strings.LiveStream_StartRecordingTitle - if let _ = videoOrientation { - text = strongSelf.presentationData.strings.LiveStream_StartRecordingTextVideo - } else { - text = strongSelf.presentationData.strings.LiveStream_StartRecordingText - } - } else { - title = strongSelf.presentationData.strings.VoiceChat_StartRecordingTitle - if let _ = videoOrientation { - text = strongSelf.presentationData.strings.VoiceChat_StartRecordingTextVideo - } else { - text = strongSelf.presentationData.strings.VoiceChat_StartRecordingText - } - } - - let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { title in - if let strongSelf = self, let title = title { - strongSelf.call.setShouldBeRecording(true, title: title, videoOrientation: videoOrientation) - - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_RecordingStarted - } else { - text = strongSelf.presentationData.strings.VoiceChat_RecordingStarted - } - - strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: text), action: { _ in return false }) - strongSelf.call.playTone(.recordingStarted) - } - }) - strongSelf.controller?.present(controller, in: .window(.root)) - } - }) - self?.controller?.present(controller, in: .window(.root)) - }))) - } - } - } - - if canManageCall { - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelLiveStream : strongSelf.presentationData.strings.VoiceChat_EndLiveStream - } else { - text = isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelVoiceChat : strongSelf.presentationData.strings.VoiceChat_EndVoiceChat - } - items.append(.action(ContextMenuActionItem(text: text, 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 title: String - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - title = isScheduled ? strongSelf.presentationData.strings.LiveStream_CancelConfirmationTitle : strongSelf.presentationData.strings.LiveStream_EndConfirmationTitle - text = isScheduled ? strongSelf.presentationData.strings.LiveStream_CancelConfirmationText : strongSelf.presentationData.strings.LiveStream_EndConfirmationText - } else { - title = isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationTitle : strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle - text = isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelConfirmationText : strongSelf.presentationData.strings.VoiceChat_EndConfirmationText - } - - let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: title, text: text, 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 { - let leaveText: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - leaveText = strongSelf.presentationData.strings.LiveStream_LeaveVoiceChat - } else { - leaveText = strongSelf.presentationData.strings.VoiceChat_LeaveVoiceChat - } - items.append(.action(ContextMenuActionItem(text: leaveText, 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) - }, iconPosition: .left, action: { [weak self] (c, _) in - guard let strongSelf = self else { - return - } - c?.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - 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: EnginePeer(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: EnginePeer(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) - }, iconPosition: .left, action: { (c, _) in - guard let strongSelf = self else { - return - } - c?.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - 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) - }, iconPosition: .left, action: { [weak self] (c, _) in - guard let strongSelf = self else { - return - } - c?.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - } - 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.wrappedGestureRecognizerDelegate - self.actionButton.view.addGestureRecognizer(longTapRecognizer) - - let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) - panRecognizer.delegate = self.wrappedGestureRecognizerDelegate - 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.ScheduleLiveStream_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] = [] - - let leaveTitle: String - let leaveAndCancelTitle: String - - if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info { - leaveTitle = self.presentationData.strings.LiveStream_LeaveConfirmation - leaveAndCancelTitle = self.isScheduled ? self.presentationData.strings.LiveStream_LeaveAndCancelVoiceChat : self.presentationData.strings.LiveStream_LeaveAndEndVoiceChat - } else { - leaveTitle = self.presentationData.strings.VoiceChat_LeaveConfirmation - leaveAndCancelTitle = self.isScheduled ? self.presentationData.strings.VoiceChat_LeaveAndCancelVoiceChat : self.presentationData.strings.VoiceChat_LeaveAndEndVoiceChat - } - - items.append(ActionSheetTextItem(title: leaveTitle)) - items.append(ActionSheetButtonItem(title: leaveAndCancelTitle, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - if let strongSelf = self { - let title: String - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - title = strongSelf.isScheduled ? strongSelf.presentationData.strings.LiveStream_CancelConfirmationTitle : strongSelf.presentationData.strings.LiveStream_EndConfirmationTitle - text = strongSelf.isScheduled ? strongSelf.presentationData.strings.LiveStream_CancelConfirmationText : strongSelf.presentationData.strings.LiveStream_EndConfirmationText - } else { - 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 - } - - if let (members, _) = strongSelf.currentCallMembers, members.count >= 10 || true { - let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: title, text: text, 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() - } - } - })) - - let leaveText: String - if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info { - leaveText = self.presentationData.strings.LiveStream_LeaveVoiceChat - } else { - leaveText = self.presentationData.strings.VoiceChat_LeaveVoiceChat - } - - items.append(ActionSheetButtonItem(title: leaveText, 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 - } - - guard let callPeerId = self.call.peerId else { - return - } - - let _ = (self.context.account.postbox.loadedPeerWithId(callPeerId) - |> 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.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).start(next: { [weak self] peerList in - if let strongSelf = self { - let peers = peerList.compactMap { $0 } - 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(title: nil, 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(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(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 - } - guard let callPeerId = strongSelf.call.peerId else { - return - } - - let _ = (strongSelf.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId), - TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId) - ) - |> map { peer, exportedInvitation -> GroupCallInviteLinks? in - if let inviteLinks = inviteLinks { - return inviteLinks - } else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty { - return GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)?voicechat", speakerLink: nil) - } else if let link = 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 - var 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) - } - - if videoFrame.width < 0.0 || videoFrame.height < 0.0 || !videoFrame.width.isNormal || !videoFrame.height.isNormal { - videoFrame = CGRect() - } - - 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) - } - } - - let participantsFrame = CGRect(x: 0.0, y: bottomCornersFrame.maxY - 100.0, width: size.width, height: 216.0) - transition.updateFrame(node: self.participantsNode, frame: participantsFrame) - self.participantsNode.update(size: participantsFrame.size, participants: self.currentTotalCount, groupingSeparator: self.presentationData.dateTimeFormat.groupingSeparator, transition: .immediate) - } - - 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.ScheduleLiveStream_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 = "" - var speaking = false - if self.scrollAtTop { - subtitle = self.currentSubtitle - speaking = false - } else { - subtitle = self.currentSpeakingSubtitle ?? self.currentSubtitle - 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 buttonWidth = min(size.width - 32.0, centralButtonSize.width) - let buttonHeight = self.scheduleCancelButton.updateLayout(width: buttonWidth, transition: .immediate) - self.scheduleCancelButton.frame = CGRect(x: floorToScreenPixels(centerButtonFrame.midX - buttonWidth / 2.0), y: 137.0, width: buttonWidth, 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: [EnginePeer], speakingPeers: Set, 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() - var processedFullscreenPeerIds = Set() - - 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 - } - -// let isLivestream: Bool -// if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info { -// isLivestream = true -// } else { -// isLivestream = false -// } - - let canManageCall = self.callState?.canManageCall ?? false - - 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 ?? true) { -// if isLivestream && !canManageCall { -// continue -// } - 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 = 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 - } - -// if isLivestream && !(memberMuteState?.canUnmute ?? true) { -// continue -// } - } - - 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: canManageCall, - 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: EnginePeer(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._asPeer(), - 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 ? EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) : EnginePeer(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() - 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.makeBlurView(input: input, mainView: videoView)) - - 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() - } - } - } - } - - 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 - } - - private var isPanningList = 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() - - let location = recognizer.location(in: self.listContainer.view) - let isPanningList: Bool - if self.listNode.frame.contains(location) { - if case let .known(value) = contentOffset, value <= 0.5 { - isPanningList = false - } else { - isPanningList = true - } - } else { - isPanningList = false - } - self.isPanningList = isPanningList - - 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 - } - - var translateBounds = false - if case .regular = layout.metrics.widthClass { - if !self.isPanningList { - 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 self.isScheduled && (translation.y < 0.0 || velocity.y < 0.0) { - return - } - - 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 case .regular = layout.metrics.widthClass, self.isPanningList { - - } else { - if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) { - if self.isScheduling { - self.dismissScheduled() - dismissing = true - } 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() { - guard let callPeerId = self.call.peerId else { - return - } - let _ = (self.context.account.postbox.loadedPeerWithId(callPeerId) - |> deliverOnMainQueue).start(next: { [weak self] chatPeer in - guard let strongSelf = self else { - return - } - - let initialTitle = strongSelf.callState?.title ?? "" - - let title: String - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - title = strongSelf.presentationData.strings.LiveStream_EditTitle - text = strongSelf.presentationData.strings.LiveStream_EditTitleText - } else { - title = strongSelf.presentationData.strings.VoiceChat_EditTitle - text = strongSelf.presentationData.strings.VoiceChat_EditTitleText - } - - let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: title, text: text, placeholder: EnginePeer(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) - - let text: String - if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = title.isEmpty ? strongSelf.presentationData.strings.LiveStream_EditTitleRemoveSuccess : strongSelf.presentationData.strings.LiveStream_EditTitleSuccess(title).string - } else { - text = title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).string - } - - strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: text), 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.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), - TelegramEngine.EngineData.Item.Configuration.SearchBots() - ) - |> 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 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, forum: false, title: nil, isSuggesting: false)! - mixin.forceDark = true - mixin.stickersContext = LegacyPaintStickersContext(context: strongSelf.context) - 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, hasVideo: false, isPersonal: false) - - 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, markup: 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, hasVideo: false, isPersonal: false) - - 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 { [weak self] subscriber in - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(postbox: account.postbox, adjustments: adjustments) - } else { - return nil - } - } - - let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") - let uploadInterface = LegacyLiveUploadInterface(context: context) - let signal: SSignal - if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), 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, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! - } else { - return SSignal.single(nil) - } - }) - - } else if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! - } 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) - - EngineTempBox.shared.dispose(tempFile) - } - } - 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 in - if peerId.namespace == Namespaces.Peer.CloudUser { - return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: nil, 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(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(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 !"".isEmpty, 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 callImpl: PresentationGroupCall - public var call: VideoChatCall { - return .group(self.callImpl) - } - private let presentationData: PresentationData - public var parentNavigationController: NavigationController? - - fileprivate let contentsReady = ValuePromise(false, ignoreRepeated: true) - fileprivate let dataReady = ValuePromise(false, ignoreRepeated: true) - fileprivate let audioOutputStateReady = ValuePromise(false, ignoreRepeated: true) - private let _ready = Promise(false) - override public var ready: Promise { - 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.callImpl = 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 }) - } - } - - func updateCall(call: VideoChatCall) { - } - - override public func loadDisplayNode() { - self.displayNode = Node(controller: self, sharedContext: self.sharedContext, call: self.callImpl) - - 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 diff --git a/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift b/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift index 5914833d9b..acb8efc5be 100644 --- a/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift +++ b/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift @@ -4,6 +4,7 @@ import SwiftSignalKit public protocol ConferenceCallE2EContextState: AnyObject { func getEmojiState() -> Data? func getParticipantIds() -> [Int64] + func getParticipants() -> [ConferenceCallE2EContext.BlockchainParticipant] func applyBlock(block: Data) func applyBroadcastBlock(block: Data) @@ -25,6 +26,16 @@ public final class ConferenceCallE2EContext { } } + public struct BlockchainParticipant: Equatable { + public let userId: Int64 + public let internalId: String + + public init(userId: Int64, internalId: String) { + self.userId = userId + self.internalId = internalId + } + } + private final class Impl { private let queue: Queue @@ -38,6 +49,7 @@ public final class ConferenceCallE2EContext { private let keyPair: TelegramKeyPair let e2eEncryptionKeyHashValue = ValuePromise(nil) + let blockchainParticipantsValue = ValuePromise<[BlockchainParticipant]>([]) private var e2ePoll0Offset: Int? private var e2ePoll0Timer: Foundation.Timer? @@ -154,7 +166,7 @@ public final class ConferenceCallE2EContext { let keyPair = self.keyPair let userId = self.userId let initializeState = self.initializeState - let (outBlocks, outEmoji) = self.state.with({ callState -> ([Data], Data) in + let (outBlocks, outEmoji, outBlockchainParticipants) = self.state.with({ callState -> ([Data], Data, [BlockchainParticipant]) in if let state = callState.state { for block in blocks { if subChainId == 0 { @@ -163,30 +175,31 @@ public final class ConferenceCallE2EContext { state.applyBroadcastBlock(block: block) } } - return (state.takeOutgoingBroadcastBlocks(), state.getEmojiState() ?? Data()) + return (state.takeOutgoingBroadcastBlocks(), state.getEmojiState() ?? Data(), state.getParticipants()) } else { if subChainId == 0 { guard let block = blocks.last else { - return ([], Data()) + return ([], Data(), []) } guard let state = initializeState(keyPair, userId, block) else { - return ([], Data()) + return ([], Data(), []) } callState.state = state for block in callState.pendingIncomingBroadcastBlocks { state.applyBroadcastBlock(block: block) } callState.pendingIncomingBroadcastBlocks.removeAll() - return (state.takeOutgoingBroadcastBlocks(), state.getEmojiState() ?? Data()) + return (state.takeOutgoingBroadcastBlocks(), state.getEmojiState() ?? Data(), state.getParticipants()) } else if subChainId == 1 { callState.pendingIncomingBroadcastBlocks.append(contentsOf: blocks) - return ([], Data()) + return ([], Data(), []) } else { - return ([], Data()) + return ([], Data(), []) } } }) self.e2eEncryptionKeyHashValue.set(outEmoji.isEmpty ? nil : outEmoji) + self.blockchainParticipantsValue.set(outBlockchainParticipants) for outBlock in outBlocks { let _ = self.engine.calls.sendConferenceCallBroadcast(callId: self.callId, accessHash: self.accessHash, block: outBlock).startStandalone() @@ -278,6 +291,7 @@ public final class ConferenceCallE2EContext { let state = self.state let callId = self.callId let accessHash = self.accessHash + let accountPeerId = engine.account.peerId if !self.pendingKickPeers.isEmpty { let pendingKickPeers = self.pendingKickPeers @@ -363,9 +377,16 @@ public final class ConferenceCallE2EContext { } // Peer ids that are in the blockchain but not in the server list - let removedPeerIds = blockchainPeerIds.filter { blockchainPeerId in - return !result.participants.contains(where: { $0.peer.id.id._internalGetInt64Value() == blockchainPeerId }) + var removedPeerIds = blockchainPeerIds.filter { blockchainPeerId in + return !result.participants.contains(where: { participant in + if case let .peer(id) = participant.id, id.namespace == Namespaces.Peer.CloudChannel, id.id._internalGetInt64Value() == blockchainPeerId { + return true + } else { + return false + } + }) } + removedPeerIds.removeAll(where: { $0 == accountPeerId.id._internalGetInt64Value() }) if removedPeerIds.isEmpty { return .single(false) @@ -422,6 +443,12 @@ public final class ConferenceCallE2EContext { return impl.e2eEncryptionKeyHashValue.get().start(next: subscriber.putNext) } } + + public var blockchainParticipants: Signal<[BlockchainParticipant], NoError> { + return self.impl.signalWith { impl, subscriber in + return impl.blockchainParticipantsValue.get().start(next: subscriber.putNext) + } + } public init(engine: TelegramEngine, callId: Int64, accessHash: Int64, userId: Int64, reference: InternalGroupCallReference, keyPair: TelegramKeyPair, initializeState: @escaping (TelegramKeyPair, Int64, Data) -> ConferenceCallE2EContextState?) { let queue = Queue.mainQueue() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 755d84711b..9305c7bb19 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -280,7 +280,7 @@ func _internal_getCurrentGroupCallInfo(account: Account, reference: InternalGrou let parsedParticipants = participants.compactMap { GroupCallParticipantsContext.Participant($0, transaction: transaction) } return ( - parsedParticipants.map(\.peer.id), + parsedParticipants.compactMap(\.peer?.id), nil ) } @@ -847,9 +847,10 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?, presentationDescription = nil } let joinedVideo = (flags & (1 << 15)) != 0 - if !state.participants.contains(where: { $0.peer.id == peer.id }) { + if !state.participants.contains(where: { $0.id == .peer(peer.id) }) { state.participants.append(GroupCallParticipantsContext.Participant( - peer: peer, + id: .peer(peer.id), + peer: EnginePeer(peer), ssrc: ssrc, videoDescription: videoDescription, presentationDescription: presentationDescription, @@ -1175,7 +1176,41 @@ public final class GroupCallParticipantsContext { } } - public var peer: Peer + public enum Id: Hashable, Comparable, CustomStringConvertible { + case peer(EnginePeer.Id) + case blockchain(String) + + public var description: String { + switch self { + case let .peer(id): + return "\(id)" + case let .blockchain(internalId): + return internalId + } + } + + public static func <(lhs: Id, rhs: Id) -> Bool { + switch lhs { + case let .peer(lhsId): + switch rhs { + case let .peer(rhsId): + return lhsId < rhsId + case .blockchain: + return true + } + case let .blockchain(lhsData): + switch rhs { + case .peer: + return false + case let .blockchain(rhsData): + return lhsData < rhsData + } + } + } + } + + public var id: Id + public var peer: EnginePeer? public var ssrc: UInt32? public var videoDescription: VideoDescription? public var presentationDescription: VideoDescription? @@ -1190,7 +1225,8 @@ public final class GroupCallParticipantsContext { public var joinedVideo: Bool public init( - peer: Peer, + id: Id, + peer: EnginePeer?, ssrc: UInt32?, videoDescription: VideoDescription?, presentationDescription: VideoDescription?, @@ -1204,6 +1240,7 @@ public final class GroupCallParticipantsContext { about: String?, joinedVideo: Bool ) { + self.id = id self.peer = peer self.ssrc = ssrc self.videoDescription = videoDescription @@ -1220,7 +1257,7 @@ public final class GroupCallParticipantsContext { } public var description: String { - return "Participant(peer: \(peer.id): \(peer.debugDisplayTitle), ssrc: \(String(describing: self.ssrc))" + return "Participant(peer: \(self.id): \(peer?.debugDisplayTitle ?? "User \(self.id)"), ssrc: \(String(describing: self.ssrc))" } public mutating func mergeActivity(from other: Participant, mergeActivityTimestamp: Bool) { @@ -1231,7 +1268,10 @@ public final class GroupCallParticipantsContext { } public static func ==(lhs: Participant, rhs: Participant) -> Bool { - if !lhs.peer.isEqual(rhs.peer) { + if lhs.id != rhs.id { + return false + } + if lhs.peer != rhs.peer { return false } if lhs.ssrc != rhs.ssrc { @@ -1318,7 +1358,7 @@ public final class GroupCallParticipantsContext { } } - return lhs.peer.id < rhs.peer.id + return lhs.id < rhs.id } } @@ -1352,13 +1392,15 @@ public final class GroupCallParticipantsContext { public mutating func mergeActivity(from other: State, myPeerId: PeerId?, previousMyPeerId: PeerId?, mergeActivityTimestamps: Bool) { var indexMap: [PeerId: Int] = [:] for i in 0 ..< other.participants.count { - indexMap[other.participants[i].peer.id] = i + if let otherParticipantPeer = other.participants[i].peer { + indexMap[otherParticipantPeer.id] = i + } } for i in 0 ..< self.participants.count { - if let index = indexMap[self.participants[i].peer.id] { + if let selfParticipantPeer = self.participants[i].peer, let index = indexMap[selfParticipantPeer.id] { self.participants[i].mergeActivity(from: other.participants[index], mergeActivityTimestamp: mergeActivityTimestamps) - if self.participants[i].peer.id == myPeerId || self.participants[i].peer.id == previousMyPeerId { + if selfParticipantPeer.id == myPeerId || selfParticipantPeer.id == previousMyPeerId { self.participants[i].joinTimestamp = other.participants[index].joinTimestamp } } @@ -1437,9 +1479,28 @@ public final class GroupCallParticipantsContext { } } + private final class ResolvedBlockchainParticipant: Equatable { + let participant: ConferenceCallE2EContext.BlockchainParticipant + let peer: EnginePeer? + + init(participant: ConferenceCallE2EContext.BlockchainParticipant, peer: EnginePeer?) { + self.participant = participant + self.peer = peer + } + + static func ==(lhs: ResolvedBlockchainParticipant, rhs: ResolvedBlockchainParticipant) -> Bool { + return lhs.participant == rhs.participant && lhs.peer == rhs.peer + } + } + + private struct BlockchainState: Equatable { + var blockchainParticipants: [ResolvedBlockchainParticipant] + } + private struct InternalState: Equatable { var state: State var overlayState: OverlayState + var blockchainState: BlockchainState } public enum Update { @@ -1547,7 +1608,7 @@ public final class GroupCallParticipantsContext { var sortAgain = false var canSeeHands = state.state.isCreator || state.state.adminIds.contains(accountPeerId) for participant in publicState.participants { - if participant.peer.id == myPeerId { + if participant.id == .peer(myPeerId) { if let muteState = participant.muteState { if muteState.canUnmute { canSeeHands = true @@ -1559,7 +1620,7 @@ public final class GroupCallParticipantsContext { } } for i in 0 ..< publicState.participants.count { - if let pendingMuteState = state.overlayState.pendingMuteStateChanges[publicState.participants[i].peer.id] { + if let participantPeer = publicState.participants[i].peer, let pendingMuteState = state.overlayState.pendingMuteStateChanges[participantPeer.id] { publicState.participants[i].muteState = pendingMuteState.state publicState.participants[i].volume = pendingMuteState.volume } @@ -1578,6 +1639,27 @@ public final class GroupCallParticipantsContext { if sortAgain { publicState.participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: publicState.sortAscending) }) } + for blockchainParticipant in state.blockchainState.blockchainParticipants { + let blockchainParticipantPeerId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(blockchainParticipant.participant.userId)) + if !publicState.participants.contains(where: { $0.id == .peer(blockchainParticipantPeerId) }) { + publicState.participants.append(Participant( + id: .peer(blockchainParticipantPeerId), + peer: blockchainParticipant.peer, + ssrc: nil, + videoDescription: nil, + presentationDescription: nil, + joinTimestamp: 0, + raiseHandRating: nil, + hasRaiseHand: false, + activityTimestamp: nil, + activityRank: nil, + muteState: nil, + volume: nil, + about: nil, + joinedVideo: false + )) + } + } return publicState } |> beforeNext { [weak self] next in @@ -1631,13 +1713,17 @@ public final class GroupCallParticipantsContext { public private(set) var serviceState: ServiceState - init(account: Account, peerId: PeerId?, myPeerId: PeerId, id: Int64, reference: InternalGroupCallReference, state: State, previousServiceState: ServiceState?) { + private var e2eStateUpdateDisposable: Disposable? + private var pendingBlockchainState: [ResolvedBlockchainParticipant]? + private var pendingApplyBlockchainStateTimer: Foundation.Timer? + + init(account: Account, peerId: PeerId?, myPeerId: PeerId, id: Int64, reference: InternalGroupCallReference, state: State, previousServiceState: ServiceState?, e2eContext: ConferenceCallE2EContext?) { self.account = account self.peerId = peerId self.myPeerId = myPeerId self.id = id self.reference = reference - self.stateValue = InternalState(state: state, overlayState: OverlayState()) + self.stateValue = InternalState(state: state, overlayState: OverlayState(), blockchainState: BlockchainState(blockchainParticipants: [])) self.statePromise = ValuePromise(self.stateValue) self.serviceState = previousServiceState ?? ServiceState() @@ -1660,7 +1746,7 @@ public final class GroupCallParticipantsContext { if let peerId { let activityCategory: PeerActivitySpace.Category = .voiceChat self.activitiesDisposable = (self.account.peerInputActivities(peerId: PeerActivitySpace(peerId: peerId, category: activityCategory)) - |> deliverOnMainQueue).start(next: { [weak self] activities in + |> deliverOnMainQueue).start(next: { [weak self] activities in guard let strongSelf = self else { return } @@ -1674,7 +1760,9 @@ public final class GroupCallParticipantsContext { var updatedParticipants = strongSelf.stateValue.state.participants var indexMap: [PeerId: Int] = [:] for i in 0 ..< updatedParticipants.count { - indexMap[updatedParticipants[i].peer.id] = i + if let participantPeer = updatedParticipants[i].peer { + indexMap[participantPeer.id] = i + } } var updated = false @@ -1717,7 +1805,8 @@ public final class GroupCallParticipantsContext { isStream: strongSelf.stateValue.state.isStream, version: strongSelf.stateValue.state.version ), - overlayState: strongSelf.stateValue.overlayState + overlayState: strongSelf.stateValue.overlayState, + blockchainState: strongSelf.stateValue.blockchainState ) } } @@ -1753,6 +1842,53 @@ public final class GroupCallParticipantsContext { } }, queue: .mainQueue()) self.activityRankResetTimer?.start() + + if let e2eContext { + let postbox = self.account.postbox + self.e2eStateUpdateDisposable = (e2eContext.blockchainParticipants + |> mapToSignal { value -> Signal<[ResolvedBlockchainParticipant], NoError> in + return postbox.transaction { transaction -> [ResolvedBlockchainParticipant] in + var result: [ResolvedBlockchainParticipant] = [] + for participant in value { + let blockchainParticipantPeerId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(participant.userId)) + if let peer = transaction.getPeer(blockchainParticipantPeerId) { + result.append(ResolvedBlockchainParticipant(participant: participant, peer: EnginePeer(peer))) + } else { + result.append(ResolvedBlockchainParticipant(participant: participant, peer: nil)) + } + } + return result + } + } + |> deliverOnMainQueue).startStrict(next: { [weak self] blockchainParticipants in + guard let self else { + return + } + + self.pendingBlockchainState = blockchainParticipants + + self.pendingApplyBlockchainStateTimer?.invalidate() + self.pendingApplyBlockchainStateTimer = nil + + var hasUnknownParticipants: Bool = false + for blockchainParticipant in blockchainParticipants { + if !self.stateValue.state.participants.contains(where: { $0.id == .peer(EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(blockchainParticipant.participant.userId))) }) { + hasUnknownParticipants = true + break + } + } + if hasUnknownParticipants { + self.pendingApplyBlockchainStateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + self.applyPendingBlockchainState() + }) + } else { + self.applyPendingBlockchainState() + } + }) + } } deinit { @@ -1764,6 +1900,19 @@ public final class GroupCallParticipantsContext { self.activityRankResetTimer?.invalidate() self.resetInviteLinksDisposable.dispose() self.subscribeDisposable.dispose() + self.e2eStateUpdateDisposable?.dispose() + self.pendingApplyBlockchainStateTimer?.invalidate() + } + + private func applyPendingBlockchainState() { + self.pendingApplyBlockchainStateTimer?.invalidate() + self.pendingApplyBlockchainStateTimer = nil + + if let pendingBlockchainState = self.pendingBlockchainState { + self.pendingBlockchainState = nil + + self.stateValue.blockchainState = BlockchainState(blockchainParticipants: pendingBlockchainState) + } } public func addUpdates(updates: [Update]) { @@ -1795,7 +1944,7 @@ public final class GroupCallParticipantsContext { public func removeLocalPeerId() { var state = self.stateValue.state - state.participants.removeAll(where: { $0.peer.id == self.myPeerId }) + state.participants.removeAll(where: { $0.id == .peer(self.myPeerId) }) self.stateValue.state = state } @@ -1822,7 +1971,9 @@ public final class GroupCallParticipantsContext { var updatedParticipants = strongSelf.stateValue.state.participants var indexMap: [PeerId: Int] = [:] for i in 0 ..< updatedParticipants.count { - indexMap[updatedParticipants[i].peer.id] = i + if let participantPeer = updatedParticipants[i].peer { + indexMap[participantPeer.id] = i + } } var updated = false @@ -1869,7 +2020,8 @@ public final class GroupCallParticipantsContext { isStream: strongSelf.stateValue.state.isStream, version: strongSelf.stateValue.state.version ), - overlayState: strongSelf.stateValue.overlayState + overlayState: strongSelf.stateValue.overlayState, + blockchainState: strongSelf.stateValue.blockchainState ) } @@ -2000,7 +2152,7 @@ public final class GroupCallParticipantsContext { for participantUpdate in update.participantUpdates { if case .left = participantUpdate.participationStatusChange { - if let index = updatedParticipants.firstIndex(where: { $0.peer.id == participantUpdate.peerId }) { + if let index = updatedParticipants.firstIndex(where: { $0.id == .peer(participantUpdate.peerId) }) { updatedParticipants.remove(at: index) updatedTotalCount = max(0, updatedTotalCount - 1) strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, canUnmute: false, joined: false)) @@ -2017,7 +2169,7 @@ public final class GroupCallParticipantsContext { var previousActivityRank: Int? var previousMuteState: GroupCallParticipantsContext.Participant.MuteState? var previousVolume: Int32? - if let index = updatedParticipants.firstIndex(where: { $0.peer.id == participantUpdate.peerId }) { + if let index = updatedParticipants.firstIndex(where: { $0.id == .peer(participantUpdate.peerId) }) { previousJoinTimestamp = updatedParticipants[index].joinTimestamp previousActivityTimestamp = updatedParticipants[index].activityTimestamp previousActivityRank = updatedParticipants[index].activityRank @@ -2054,7 +2206,8 @@ public final class GroupCallParticipantsContext { } } let participant = Participant( - peer: peer, + id: .peer(peer.id), + peer: EnginePeer(peer), ssrc: participantUpdate.ssrc, videoDescription: participantUpdate.videoDescription, presentationDescription: participantUpdate.presentationDescription, @@ -2111,7 +2264,8 @@ public final class GroupCallParticipantsContext { isStream: isStream, version: update.version ), - overlayState: updatedOverlayState + overlayState: updatedOverlayState, + blockchainState: strongSelf.stateValue.blockchainState ) strongSelf.endedProcessingUpdate() @@ -2158,7 +2312,7 @@ public final class GroupCallParticipantsContext { } for participant in self.stateValue.state.participants { - if participant.peer.id == peerId { + if participant.id == .peer(peerId) { var raiseHandEqual: Bool = true if let raiseHand = raiseHand { raiseHandEqual = (participant.raiseHandRating == nil && !raiseHand) || @@ -2744,14 +2898,14 @@ func _internal_updatedCurrentPeerGroupCall(postbox: Postbox, network: Network, a private func mergeAndSortParticipants(current currentParticipants: [GroupCallParticipantsContext.Participant], with updatedParticipants: [GroupCallParticipantsContext.Participant], sortAscending: Bool) -> [GroupCallParticipantsContext.Participant] { var mergedParticipants = currentParticipants - var existingParticipantIndices: [PeerId: Int] = [:] + var existingParticipantIndices: [GroupCallParticipantsContext.Participant.Id: Int] = [:] for i in 0 ..< mergedParticipants.count { - existingParticipantIndices[mergedParticipants[i].peer.id] = i + existingParticipantIndices[mergedParticipants[i].id] = i } for participant in updatedParticipants { - if let _ = existingParticipantIndices[participant.peer.id] { + if let _ = existingParticipantIndices[participant.id] { } else { - existingParticipantIndices[participant.peer.id] = mergedParticipants.count + existingParticipantIndices[participant.id] = mergedParticipants.count mergedParticipants.append(participant) } } @@ -2942,7 +3096,8 @@ extension GroupCallParticipantsContext.Participant { let joinedVideo = (flags & (1 << 15)) != 0 self.init( - peer: peer, + id: .peer(peer.id), + peer: EnginePeer(peer), ssrc: ssrc, videoDescription: videoDescription, presentationDescription: presentationDescription, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift index 77fc5f7ffc..3730b5c2cd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift @@ -144,8 +144,8 @@ public extension TelegramEngine { return _internal_getVideoBroadcastPart(dataSource: dataSource, callId: callId, accessHash: accessHash, timestampIdMilliseconds: timestampIdMilliseconds, durationMilliseconds: durationMilliseconds, channelId: channelId, quality: quality) } - public func groupCall(peerId: PeerId?, myPeerId: PeerId, id: Int64, reference: InternalGroupCallReference, state: GroupCallParticipantsContext.State, previousServiceState: GroupCallParticipantsContext.ServiceState?) -> GroupCallParticipantsContext { - return GroupCallParticipantsContext(account: self.account, peerId: peerId, myPeerId: myPeerId, id: id, reference: reference, state: state, previousServiceState: previousServiceState) + public func groupCall(peerId: PeerId?, myPeerId: PeerId, id: Int64, reference: InternalGroupCallReference, state: GroupCallParticipantsContext.State, previousServiceState: GroupCallParticipantsContext.ServiceState?, e2eContext: ConferenceCallE2EContext?) -> GroupCallParticipantsContext { + return GroupCallParticipantsContext(account: self.account, peerId: peerId, myPeerId: myPeerId, id: id, reference: reference, state: state, previousServiceState: previousServiceState, e2eContext: e2eContext) } public func serverTime() -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift index 1bf370f92d..e32f60f2cc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift @@ -172,7 +172,9 @@ func _internal_joinCallLinkInformation(_ hash: String, account: Account) -> Sign } var members: [EnginePeer] = [] for participant in call.topParticipants { - members.append(EnginePeer(participant.peer)) + if let peer = participant.peer { + members.append(peer) + } } return .single(JoinCallLinkInformation(id: call.info.id, accessHash: call.info.accessHash, inviter: nil, members: members, totalMemberCount: call.info.participantCount)) } @@ -198,7 +200,9 @@ func _internal_joinCallInvitationInformation(account: Account, messageId: Messag } var members: [EnginePeer] = [] for participant in call.topParticipants { - members.append(EnginePeer(participant.peer)) + if let peer = participant.peer { + members.append(peer) + } } return .single(JoinCallLinkInformation(id: call.info.id, accessHash: call.info.accessHash, inviter: nil, members: members, totalMemberCount: call.info.participantCount)) } diff --git a/submodules/TelegramUIPreferences/Sources/CallListSettings.swift b/submodules/TelegramUIPreferences/Sources/CallListSettings.swift index 543664e00b..850f6c622f 100644 --- a/submodules/TelegramUIPreferences/Sources/CallListSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/CallListSettings.swift @@ -4,41 +4,30 @@ import SwiftSignalKit public struct CallListSettings: Codable, Equatable { public var _showTab: Bool? - public var defaultShowTab: Bool? public static var defaultSettings: CallListSettings { - return CallListSettings(showTab: false) + return CallListSettings(showTab: nil) } public var showTab: Bool { get { if let value = self._showTab { return value - } else if let defaultValue = self.defaultShowTab { - return defaultValue } else { - return CallListSettings.defaultSettings.showTab + return true } } set { self._showTab = newValue } } - public init(showTab: Bool) { + public init(showTab: Bool?) { self._showTab = showTab } - public init(showTab: Bool?, defaultShowTab: Bool?) { - self._showTab = showTab - self.defaultShowTab = defaultShowTab - } - public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) - if let alternativeDefaultValue = try container.decodeIfPresent(Int32.self, forKey: "defaultShowTab") { - self.defaultShowTab = alternativeDefaultValue != 0 - } if let value = try container.decodeIfPresent(Int32.self, forKey: "showTab") { self._showTab = value != 0 } @@ -47,11 +36,6 @@ public struct CallListSettings: Codable, Equatable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) - if let defaultShowTab = self.defaultShowTab { - try container.encode((defaultShowTab ? 1 : 0) as Int32, forKey: "defaultShowTab") - } else { - try container.encodeNil(forKey: "defaultShowTab") - } if let showTab = self._showTab { try container.encode((showTab ? 1 : 0) as Int32, forKey: "showTab") } else { @@ -60,11 +44,11 @@ public struct CallListSettings: Codable, Equatable { } public static func ==(lhs: CallListSettings, rhs: CallListSettings) -> Bool { - return lhs._showTab == rhs._showTab && lhs.defaultShowTab == rhs.defaultShowTab + return lhs._showTab == rhs._showTab } public func withUpdatedShowTab(_ showTab: Bool) -> CallListSettings { - return CallListSettings(showTab: showTab, defaultShowTab: self.defaultShowTab) + return CallListSettings(showTab: showTab) } } diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 579cae3c3c..36161286bd 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 579cae3c3c70c6ed8bc0d88cc41de28bd50a7f1b +Subproject commit 36161286bd7fae1b9bb2e8dad817ae9af6d68055 diff --git a/third-party/td/TdBinding/Public/TdBinding/TdBinding.h b/third-party/td/TdBinding/Public/TdBinding/TdBinding.h index 30afa7e265..f871989564 100644 --- a/third-party/td/TdBinding/Public/TdBinding/TdBinding.h +++ b/third-party/td/TdBinding/Public/TdBinding/TdBinding.h @@ -22,10 +22,10 @@ NS_ASSUME_NONNULL_BEGIN @interface TdCallParticipant : NSObject -@property (nonatomic, strong, readonly) NSData *publicKey; +@property (nonatomic, strong, readonly) NSString *internalId; @property (nonatomic, readonly) int64_t userId; -- (nullable instancetype)initWithPublicKey:(NSData *)publicKey userId:(int64_t)userId; +- (nullable instancetype)initWithInternalId:(NSString *)internalId userId:(int64_t)userId; @end @@ -35,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *)takeOutgoingBroadcastBlocks; - (NSData *)emojiState; -- (NSArray *)participantIds; +- (NSArray *)participants; - (void)applyBlock:(NSData *)block; - (void)applyBroadcastBlock:(NSData *)block; diff --git a/third-party/td/TdBinding/Sources/TdBinding.mm b/third-party/td/TdBinding/Sources/TdBinding.mm index 141c513ad7..434143ae44 100644 --- a/third-party/td/TdBinding/Sources/TdBinding.mm +++ b/third-party/td/TdBinding/Sources/TdBinding.mm @@ -58,10 +58,10 @@ static NSString *hexStringFromData(NSData *data) { @implementation TdCallParticipant -- (nullable instancetype)initWithPublicKey:(NSData *)publicKey userId:(int64_t)userId { +- (nullable instancetype)initWithInternalId:(NSString *)internalId userId:(int64_t)userId { self = [super init]; if (self != nil) { - _publicKey = publicKey; + _internalId = internalId; _userId = userId; } return self; @@ -176,17 +176,18 @@ static NSString *hexStringFromData(NSData *data) { return outEmojiHash; } -- (NSArray *)participantIds { +- (NSArray *)participants { auto result = tde2e_api::call_get_state(_callId); if (!result.is_ok()) { return @[]; } auto state = result.value(); - NSMutableArray *participantIds = [[NSMutableArray alloc] init]; + NSMutableArray *participants = [[NSMutableArray alloc] init]; for (const auto &it : state.participants) { - [participantIds addObject:[NSNumber numberWithLongLong:it.user_id]]; + NSString *internalId = [[NSString alloc] initWithFormat:@"%lld", it.public_key_id]; + [participants addObject:[[TdCallParticipant alloc] initWithInternalId:internalId userId:it.user_id]]; } - return participantIds; + return participants; } - (void)applyBlock:(NSData *)block { diff --git a/third-party/td/td b/third-party/td/td index a03a90470d..04adfc87de 160000 --- a/third-party/td/td +++ b/third-party/td/td @@ -1 +1 @@ -Subproject commit a03a90470d6fca9a5a3db747ba3f3e4a465b5fe7 +Subproject commit 04adfc87deea4c804def118e88c89a08c388b32b diff --git a/versions.json b/versions.json index d6ff229d25..180c7987ae 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.9.1", + "app": "11.10", "xcode": "16.2", "bazel": "7.3.1:981f82a470bad1349322b6f51c9c6ffa0aa291dab1014fac411543c12e661dff", "macos": "15"