diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index 855db3d562..50825f66db 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/AnimatedCountLabelNode:AnimatedCountLabelNode", "//submodules/DeviceProximity:DeviceProximity", "//submodules/ManagedAnimationNode:ManagedAnimationNode", + "//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index cbbe9c5d12..5f36aaf154 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -16,6 +16,7 @@ import UniversalMediaPlayer import AccountContext import DeviceProximity import UndoUI +import TemporaryCachedPeerDataManager private extension GroupCallParticipantsContext.Participant { var allSsrcs: Set { @@ -353,10 +354,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { public let peerId: PeerId private let invite: String? private var joinAsPeerId: PeerId + private var ignorePreviousJoinAsPeerId: (PeerId, UInt32)? public private(set) var isVideo: Bool - private let temporaryJoinTimestamp: Int32 + private var temporaryJoinTimestamp: Int32 + private var temporaryActivityTimestamp: Double? + private var temporaryActivityRank: Int? private var internalState: InternalState = .requesting private let internalStatePromise = Promise(.requesting) @@ -526,6 +530,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var processedMissingSsrcs = Set() private let missingSsrcsDisposable = MetaDisposable() private var isRequestingMissingSsrcs: Bool = false + + private var peerUpdatesSubscription: Disposable? init( accountContext: AccountContext, @@ -738,11 +744,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title)) }) { - if let participantsContext = temporaryParticipantsContext.context.participantsContext, let immediateState = participantsContext.immediateState { - self.switchToTemporaryParticipantsContext(sourceContext: participantsContext, initialState: immediateState, oldMyPeerId: self.joinAsPeerId) - } + self.switchToTemporaryParticipantsContext(sourceContext: temporaryParticipantsContext.context.participantsContext, oldMyPeerId: self.joinAsPeerId) } else { - self.updateSessionState(internalState: .requesting, audioSessionControl: nil) + self.switchToTemporaryParticipantsContext(sourceContext: nil, oldMyPeerId: self.joinAsPeerId) } self.removedChannelMembersDisposable = (accountContext.peerChannelMemberCategoriesContextsManager.removedChannelMembers @@ -759,6 +763,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { let _ = (self.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self else { + return + } var canManageCall = false if let peer = peer as? TelegramGroup { if case .creator = peer.role { @@ -772,14 +779,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } else if (peer.adminRights?.rights.contains(.canManageCalls) == true) { canManageCall = true } + strongSelf.peerUpdatesSubscription = strongSelf.accountContext.account.viewTracker.polledChannel(peerId: peer.id).start() } - if let strongSelf = self { - var updatedValue = strongSelf.stateValue - updatedValue.canManageCall = canManageCall - strongSelf.stateValue = updatedValue - } + var updatedValue = strongSelf.stateValue + updatedValue.canManageCall = canManageCall + strongSelf.stateValue = updatedValue }) - self.requestCall(movingFromBroadcastToRtc: false) } @@ -813,9 +818,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.audioOutputStateDisposable?.dispose() self.removedChannelMembersDisposable?.dispose() + + self.peerUpdatesSubscription?.dispose() } - private func switchToTemporaryParticipantsContext(sourceContext: GroupCallParticipantsContext, initialState: GroupCallParticipantsContext.State, oldMyPeerId: PeerId) { + private func switchToTemporaryParticipantsContext(sourceContext: GroupCallParticipantsContext?, oldMyPeerId: PeerId) { let myPeerId = self.joinAsPeerId let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in if let peer = transaction.getPeer(myPeerId) { @@ -824,101 +831,166 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return nil } } - let temporaryParticipantsContext = GroupCallParticipantsContext(account: self.account, peerId: self.peerId, myPeerId: myPeerId, id: sourceContext.id, accessHash: sourceContext.accessHash, state: initialState) - self.temporaryParticipantsContext = temporaryParticipantsContext - self.participantsContextStateDisposable.set((combineLatest(queue: .mainQueue(), - myPeer, - temporaryParticipantsContext.state, - temporaryParticipantsContext.activeSpeakers - ) - |> take(1)).start(next: { [weak self] myPeerAndCachedData, state, activeSpeakers in - guard let strongSelf = self else { - return - } - - var topParticipants: [GroupCallParticipantsContext.Participant] = [] - - var members = PresentationGroupCallMembers( - participants: [], - speakingParticipants: [], - totalCount: 0, - loadMoreToken: nil + if let sourceContext = sourceContext, let initialState = sourceContext.immediateState { + let temporaryParticipantsContext = GroupCallParticipantsContext(account: self.account, peerId: self.peerId, myPeerId: myPeerId, id: sourceContext.id, accessHash: sourceContext.accessHash, state: initialState) + self.temporaryParticipantsContext = temporaryParticipantsContext + self.participantsContextStateDisposable.set((combineLatest(queue: .mainQueue(), + myPeer, + temporaryParticipantsContext.state, + temporaryParticipantsContext.activeSpeakers ) - - var updatedInvitedPeers = strongSelf.invitedPeersValue - var didUpdateInvitedPeers = false - - var participants = state.participants - - if oldMyPeerId != myPeerId { - for i in 0 ..< participants.count { - if participants[i].peer.id == oldMyPeerId { - participants.remove(at: i) - break + |> take(1)).start(next: { [weak self] myPeerAndCachedData, state, activeSpeakers in + guard let strongSelf = self else { + return + } + + var topParticipants: [GroupCallParticipantsContext.Participant] = [] + + var members = PresentationGroupCallMembers( + participants: [], + speakingParticipants: [], + totalCount: 0, + loadMoreToken: nil + ) + + var updatedInvitedPeers = strongSelf.invitedPeersValue + var didUpdateInvitedPeers = false + + var participants = state.participants + + if oldMyPeerId != myPeerId { + for i in 0 ..< participants.count { + if participants[i].peer.id == oldMyPeerId { + participants.remove(at: i) + break + } } } - } - - if !participants.contains(where: { $0.peer.id == myPeerId }) { - if let (myPeer, cachedData) = myPeerAndCachedData { - let about: String? - if let cachedData = cachedData as? CachedUserData { - about = cachedData.about - } else if let cachedData = cachedData as? CachedChannelData { - about = cachedData.about - } else { - about = nil + + if !participants.contains(where: { $0.peer.id == myPeerId }) { + if let (myPeer, cachedData) = myPeerAndCachedData { + let about: String? + if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else { + about = nil + } + participants.append(GroupCallParticipantsContext.Participant( + peer: myPeer, + ssrc: nil, + jsonParams: nil, + joinTimestamp: strongSelf.temporaryJoinTimestamp, + raiseHandRating: nil, + hasRaiseHand: false, + activityTimestamp: strongSelf.temporaryActivityTimestamp, + activityRank: strongSelf.temporaryActivityRank, + muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), + volume: nil, + about: about + )) + participants.sort() } - participants.insert(GroupCallParticipantsContext.Participant( - peer: myPeer, - ssrc: nil, - jsonParams: nil, - joinTimestamp: strongSelf.temporaryJoinTimestamp, - raiseHandRating: nil, - hasRaiseHand: false, - activityTimestamp: nil, - activityRank: nil, - muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), - volume: nil, - about: about - ), at: 0) } - } - - for participant in participants { - members.participants.append(participant) - - if topParticipants.count < 3 { - topParticipants.append(participant) + + for participant in participants { + members.participants.append(participant) + + if topParticipants.count < 3 { + topParticipants.append(participant) + } + + if let index = updatedInvitedPeers.firstIndex(of: participant.peer.id) { + updatedInvitedPeers.remove(at: index) + didUpdateInvitedPeers = true + } } - - if let index = updatedInvitedPeers.firstIndex(of: participant.peer.id) { - updatedInvitedPeers.remove(at: index) - didUpdateInvitedPeers = true + + members.totalCount = state.totalCount + members.loadMoreToken = state.nextParticipantsFetchOffset + + strongSelf.membersValue = members + + var stateValue = strongSelf.stateValue + stateValue.myPeerId = strongSelf.joinAsPeerId + stateValue.adminIds = state.adminIds + + strongSelf.stateValue = stateValue + + strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState( + participantCount: state.totalCount, + topParticipants: topParticipants, + activeSpeakers: activeSpeakers + ))) + + if didUpdateInvitedPeers { + strongSelf.invitedPeersValue = updatedInvitedPeers } - } - - members.totalCount = state.totalCount - members.loadMoreToken = state.nextParticipantsFetchOffset - - strongSelf.membersValue = members - - var stateValue = strongSelf.stateValue - stateValue.myPeerId = strongSelf.joinAsPeerId - stateValue.adminIds = state.adminIds - - strongSelf.stateValue = stateValue - - strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState( - participantCount: state.totalCount, - topParticipants: topParticipants, - activeSpeakers: activeSpeakers - ))) - - if didUpdateInvitedPeers { - strongSelf.invitedPeersValue = updatedInvitedPeers - } - })) + })) + } else { + self.temporaryParticipantsContext = nil + self.participantsContextStateDisposable.set((myPeer + |> deliverOnMainQueue + |> take(1)).start(next: { [weak self] myPeerAndCachedData in + guard let strongSelf = self else { + return + } + + var topParticipants: [GroupCallParticipantsContext.Participant] = [] + + var members = PresentationGroupCallMembers( + participants: [], + speakingParticipants: [], + totalCount: 0, + loadMoreToken: nil + ) + + var participants: [GroupCallParticipantsContext.Participant] = [] + + if !participants.contains(where: { $0.peer.id == myPeerId }) { + if let (myPeer, cachedData) = myPeerAndCachedData { + let about: String? + if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else { + about = nil + } + participants.append(GroupCallParticipantsContext.Participant( + peer: myPeer, + ssrc: nil, + jsonParams: nil, + joinTimestamp: strongSelf.temporaryJoinTimestamp, + raiseHandRating: nil, + hasRaiseHand: false, + activityTimestamp: strongSelf.temporaryActivityTimestamp, + activityRank: strongSelf.temporaryActivityRank, + muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), + volume: nil, + about: about + )) + participants.sort() + } + } + + for participant in participants { + members.participants.append(participant) + + if topParticipants.count < 3 { + topParticipants.append(participant) + } + } + + strongSelf.membersValue = members + + var stateValue = strongSelf.stateValue + stateValue.myPeerId = strongSelf.joinAsPeerId + + strongSelf.stateValue = stateValue + })) + } } private func updateSessionState(internalState: InternalState, audioSessionControl: ManagedAudioSessionControl?) { @@ -1006,6 +1078,35 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { guard let strongSelf = self else { return } + + let peerAdminIds: Signal<[PeerId], NoError> + let peerId = strongSelf.peerId + if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel { + peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in + var result: [PeerId] = [] + if let entry = transaction.retrieveItemCacheEntry(id: cachedChannelAdminRanksEntryId(peerId: peerId)) as? CachedChannelAdminRanks { + result = Array(entry.ranks.keys) + } + return result + } + } else { + peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in + var result: [PeerId] = [] + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData { + if let participants = cachedData.participants { + for participant in participants.participants { + if case .creator = participant { + result.append(participant.peerId) + } else if case .admin = participant { + result.append(participant.peerId) + } + } + } + } + return result + } + } + strongSelf.currentLocalSsrc = ssrc strongSelf.requestDisposable.set((joinGroupCall( account: strongSelf.account, @@ -1015,6 +1116,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { accessHash: callInfo.accessHash, preferMuted: true, joinPayload: joinPayload, + peerAdminIds: peerAdminIds, inviteHash: strongSelf.invite ) |> deliverOnMainQueue).start(next: { joinCallResult in @@ -1276,8 +1378,18 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { var updatedInvitedPeers = strongSelf.invitedPeersValue var didUpdateInvitedPeers = false - + var participants = state.participants + + if let (ignorePeerId, ignoreSsrc) = strongSelf.ignorePreviousJoinAsPeerId { + for i in 0 ..< participants.count { + if participants[i].peer.id == ignorePeerId && participants[i].ssrc == ignoreSsrc { + participants.remove(at: i) + break + } + } + } + if !participants.contains(where: { $0.peer.id == myPeerId }) { if let (myPeer, cachedData) = myPeerAndCachedData { let about: String? @@ -1288,6 +1400,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } else { about = nil } + participants.append(GroupCallParticipantsContext.Participant( peer: myPeer, ssrc: nil, @@ -1295,8 +1408,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { joinTimestamp: strongSelf.temporaryJoinTimestamp, raiseHandRating: nil, hasRaiseHand: false, - activityTimestamp: nil, - activityRank: nil, + activityTimestamp: strongSelf.temporaryActivityTimestamp, + activityRank: strongSelf.temporaryActivityRank, muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), volume: nil, about: about @@ -1613,10 +1726,20 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } let previousPeerId = strongSelf.joinAsPeerId + if let localSsrc = strongSelf.currentLocalSsrc { + strongSelf.ignorePreviousJoinAsPeerId = (previousPeerId, localSsrc) + } strongSelf.joinAsPeerId = peerId if let participantsContext = strongSelf.participantsContext, let immediateState = participantsContext.immediateState { - strongSelf.switchToTemporaryParticipantsContext(sourceContext: participantsContext, initialState: immediateState, oldMyPeerId: previousPeerId) + for participant in immediateState.participants { + if participant.peer.id == previousPeerId { + strongSelf.temporaryJoinTimestamp = participant.joinTimestamp + strongSelf.temporaryActivityTimestamp = participant.activityTimestamp + strongSelf.temporaryActivityRank = participant.activityRank + } + } + strongSelf.switchToTemporaryParticipantsContext(sourceContext: participantsContext, oldMyPeerId: previousPeerId) } else { strongSelf.stateValue.myPeerId = peerId } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 856434d606..1e49bfeecd 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -276,7 +276,7 @@ private final class MainVideoContainerNode: ASDisplayNode { return } self.currentPeer = peer - if let (peerId, source) = peer { + if let (_, source) = peer { self.call.makeIncomingVideoView(source: source, completion: { [weak self] videoView in Queue.mainQueue().async { guard let strongSelf = self, let videoView = videoView else { @@ -704,6 +704,9 @@ public final class VoiceChatController: ViewController { private var requestedVideoSources = Set() private var videoNodes: [(PeerId, UInt32, GroupVideoNode)] = [] + + private let displayAsPeersPromise = Promise<[FoundPeer]>([]) + private let inviteLinksPromise = Promise(nil) private var currentDominantSpeakerWithVideo: (PeerId, UInt32)? @@ -729,9 +732,9 @@ public final class VoiceChatController: ViewController { self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor self.backgroundNode.clipsToBounds = false - if false { + /*if false { self.mainVideoContainer = MainVideoContainerNode(context: call.accountContext, call: call) - } + }*/ self.listNode = ListView() self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3) @@ -816,17 +819,15 @@ public final class VoiceChatController: ViewController { return result } ) - let displayAsPeersPromise = Promise<[FoundPeer]>([]) - displayAsPeersPromise.set(displayAsPeers) - - let inviteLinksPromise = Promise(nil) - inviteLinksPromise.set(.single(nil) + 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) }, openPeer: { [weak self] peerId in - if let strongSelf = self, let navigationController = strongSelf.controller?.parentNavigationController { + if let strongSelf = self { /*let context = strongSelf.context strongSelf.controller?.dismiss(completion: { Queue.mainQueue().justDispatch { @@ -864,7 +865,7 @@ public final class VoiceChatController: ViewController { return transaction.getPeer(groupPeerId) } - let _ = combineLatest(queue: Queue.mainQueue(), groupPeer, inviteLinksPromise.get() |> take(1)).start(next: { groupPeer, inviteLinks in + let _ = combineLatest(queue: Queue.mainQueue(), groupPeer, strongSelf.inviteLinksPromise.get() |> take(1)).start(next: { groupPeer, inviteLinks in guard let strongSelf = self else { return } @@ -1368,7 +1369,7 @@ public final class VoiceChatController: ViewController { self.call.state, self.call.members, invitedPeers, - displayAsPeersPromise.get() + self.displayAsPeersPromise.get() ) |> mapToThrottled { values in return .single(values) @@ -1514,272 +1515,9 @@ public final class VoiceChatController: ViewController { self.audioOutputNode.addTarget(self, action: #selector(self.audioOutputPressed), forControlEvents: .touchUpInside) self.cameraButtonNode.addTarget(self, action: #selector(self.cameraPressed), forControlEvents: .touchUpInside) - - let avatarSize = CGSize(width: 28.0, height: 28.0) + self.optionsButton.contextAction = { [weak self] sourceNode, gesture in - guard let strongSelf = self, let controller = strongSelf.controller else { - return - } - - let canManageCall = !strongSelf.optionsButtonIsAvatar - - let myPeerId = strongSelf.callState?.myPeerId - let darkTheme = strongSelf.darkTheme - - var mainItemsImpl: (() -> Signal<[ContextMenuItem], NoError>)? - - let displayAsItems: () -> Signal<[ContextMenuItem], NoError> = { - return displayAsPeersPromise.get() - |> take(1) - |> map { peers -> [ContextMenuItem] in - var items: [ContextMenuItem] = [] - items.append(.custom(VoiceChatInfoContextItem(text: 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 { - subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers) - } - - let isSelected = peer.peer.id == myPeerId - let extendedAvatarSize = CGSize(width: 35.0, height: 35.0) - let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize) - |> map { image -> UIImage? in - if isSelected, let image = image { - return generateImage(extendedAvatarSize, rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height)) - - let lineWidth = 1.0 + UIScreenPixel - context.setLineWidth(lineWidth) - context.setStrokeColor(darkTheme.actionSheet.controlAccentColor.cgColor) - context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) - }) - } else { - return image - } - } - - items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { _, f in - f(.default) - - guard let strongSelf = self else { - return - } - - if peer.peer.id != myPeerId { - strongSelf.call.reconnect(as: peer.peer.id) - - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer.peer, text: strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false }) - } - }))) - - if peer.peer.id.namespace == Namespaces.Peer.CloudUser { - items.append(.separator) - } - } - if canManageCall { - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, action: { (c, _) in - if let mainItems = mainItemsImpl { - c.setItems(mainItems()) - } - }))) - } - return items - } - } - - let permissionItems: () -> Signal<[ContextMenuItem], NoError> = { - var items: [ContextMenuItem] = [] - if let callState = strongSelf.callState, callState.canManageCall, let defaultParticipantMuteState = callState.defaultParticipantMuteState { - let isMuted = defaultParticipantMuteState == .muted - - items.append(.action(ContextMenuActionItem(text: strongSelf.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: { _, f in - f(.dismissWithoutContent) - - guard let strongSelf = self else { - return - } - strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: false) - }))) - items.append(.action(ContextMenuActionItem(text: strongSelf.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: { _, f in - f(.dismissWithoutContent) - - guard let strongSelf = self else { - return - } - strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: true) - }))) - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, action: { (c, _) in - if let mainItems = mainItemsImpl { - c.setItems(mainItems()) - } - }))) - } - return .single(items) - } - - mainItemsImpl = { - return combineLatest(displayAsPeersPromise.get(), context.account.postbox.loadedPeerWithId(call.peerId), inviteLinksPromise.get()) - |> take(1) - |> map { peers, chatPeer, inviteLinks -> [ContextMenuItem] in - let presentationData = strongSelf.presentationData - var items: [ContextMenuItem] = [] - - if peers.count > 1 { - for peer in peers { - if peer.peer.id == myPeerId { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, _ in - c.setItems(displayAsItems()) - }))) - items.append(.separator) - break - } - } - } - - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditTitle, icon: { theme -> UIImage? in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) - }, action: { [weak self] _, f in - f(.default) - - let controller = voiceChatTitleEditController(sharedContext: context.sharedContext, account: context.account, forceTheme: self?.darkTheme, title: presentationData.strings.VoiceChat_EditTitleTitle, text: presentationData.strings.VoiceChat_EditTitleText, placeholder: presentationData.strings.VoiceChat_Title, value: self?.callState?.title, apply: { [weak self] title in - if let strongSelf = self, let title = title { - strongSelf.call.updateTitle(title) - - strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false }) - } - }) - self?.controller?.present(controller, in: .window(.root)) - }))) - - 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 - c.setItems(permissionItems()) - }))) - } - - 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: { [weak self] _, f in - f(.default) - - self?.presentShare(inviteLinks) - }))) - } - - if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp { - items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { _, f in - f(.dismissWithoutContent) - - 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: { [weak self] in - if let strongSelf = self { - strongSelf.call.setShouldBeRecording(false, title: nil) - - strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: strongSelf.presentationData.strings.VoiceChat_RecordingSaved), action: { _ in return false }) - } - })]) - self?.controller?.present(alertController, in: .window(.root)) - }), false)) - } else { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in - return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.dismissWithoutContent) - - - let controller = voiceChatTitleEditController(sharedContext: context.sharedContext, account: context.account, forceTheme: self?.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, apply: { [weak self] title in - if let strongSelf = self, let title = title { - strongSelf.call.setShouldBeRecording(true, title: title) - - strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false }) - } - }) - self?.controller?.present(controller, in: .window(.root)) - }))) - } - - if let callState = strongSelf.callState, callState.canManageCall { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) - }, action: { _, f in - f(.dismissWithoutContent) - - guard let strongSelf = self else { - return - } - - let action: () -> Void = { - guard let strongSelf = self else { - return - } - - let _ = (strongSelf.call.leave(terminateIfPossible: true) - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(completed: { - self?.controller?.dismiss() - }) - } - - let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle, text: strongSelf.presentationData.strings.VoiceChat_EndConfirmationText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.VoiceChat_EndConfirmationEnd, action: { - action() - })]) - strongSelf.controller?.present(alertController, in: .window(.root)) - }))) - } - - - return items - } - } - - let items: Signal<[ContextMenuItem], NoError> - if canManageCall { - items = mainItemsImpl?() ?? .single([]) - } else { - items = displayAsItems() - } - - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: strongSelf.optionsButton.referenceNode)), items: items, reactionItems: [], gesture: gesture) - strongSelf.controller?.presentInGlobalOverlay(contextController) + self?.openContextMenu(sourceNode: sourceNode, gesture: gesture) } self.optionsButton.addTarget(self, action: #selector(self.optionsPressed), forControlEvents: .touchUpInside) @@ -1933,6 +1671,307 @@ public final class VoiceChatController: ViewController { self.memberEventsDisposable.dispose() self.voiceSourcesDisposable.dispose() } + + private func openContextMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { + let canManageCall = !self.optionsButtonIsAvatar + + let items: Signal<[ContextMenuItem], NoError> + if canManageCall { + items = self.contextMenuMainItems() + } else { + items = self.contextMenuDisplayAsItems() + } + + if let controller = self.controller { + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: self.optionsButton.referenceNode)), items: items, reactionItems: [], gesture: gesture) + controller.presentInGlobalOverlay(contextController) + } + } + + private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> { + guard let myPeerId = self.callState?.myPeerId else { + return .single([]) + } + + let avatarSize = CGSize(width: 28.0, height: 28.0) + + return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(call.peerId), self.inviteLinksPromise.get()) + |> take(1) + |> deliverOnMainQueue + |> map { [weak self] peers, chatPeer, inviteLinks -> [ContextMenuItem] in + guard let strongSelf = self else { + return [] + } + + let presentationData = strongSelf.presentationData + var items: [ContextMenuItem] = [] + + if peers.count > 1 { + for peer in peers { + if peer.peer.id == myPeerId { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, _ in + guard let strongSelf = self else { + return + } + c.setItems(strongSelf.contextMenuDisplayAsItems()) + }))) + items.append(.separator) + break + } + } + } + + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditTitle, icon: { theme -> UIImage? in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + guard let strongSelf = self else { + return + } + + let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditTitleTitle, text: presentationData.strings.VoiceChat_EditTitleText, placeholder: presentationData.strings.VoiceChat_Title, value: strongSelf.callState?.title, apply: { title in + if let strongSelf = self, let title = title { + strongSelf.call.updateTitle(title) + + strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false }) + } + }) + self?.controller?.present(controller, in: .window(.root)) + }))) + + var hasPermissions = true + if let chatPeer = chatPeer as? TelegramChannel { + if case .broadcast = chatPeer.info { + hasPermissions = false + } else if chatPeer.flags.contains(.isGigagroup) { + hasPermissions = false + } + } + if hasPermissions { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + }, action: { c, _ in + guard let strongSelf = self else { + return + } + c.setItems(strongSelf.contextMenuPermissionItems()) + }))) + } + + if let inviteLinks = inviteLinks { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + self?.presentShare(inviteLinks) + }))) + } + + 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) + + strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: strongSelf.presentationData.strings.VoiceChat_RecordingSaved), action: { _ in return false }) + } + })]) + self?.controller?.present(alertController, in: .window(.root)) + }), false)) + } else { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in + return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + + let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, apply: { title in + if let strongSelf = self, let title = title { + strongSelf.call.setShouldBeRecording(true, title: title) + + strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false }) + } + }) + self?.controller?.present(controller, in: .window(.root)) + }))) + } + + if let callState = strongSelf.callState, callState.canManageCall { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + + let action: () -> Void = { + guard let strongSelf = self else { + return + } + + let _ = (strongSelf.call.leave(terminateIfPossible: true) + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(completed: { + self?.controller?.dismiss() + }) + } + + let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle, text: strongSelf.presentationData.strings.VoiceChat_EndConfirmationText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.VoiceChat_EndConfirmationEnd, action: { + action() + })]) + strongSelf.controller?.present(alertController, in: .window(.root)) + }))) + } + + + return 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 canManageCall = !self.optionsButtonIsAvatar + 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] = [] + items.append(.custom(VoiceChatInfoContextItem(text: 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 { + subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers) + } + + let isSelected = peer.peer.id == myPeerId + let extendedAvatarSize = CGSize(width: 35.0, height: 35.0) + let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize) + |> map { image -> UIImage? in + if isSelected, let image = image { + return generateImage(extendedAvatarSize, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height)) + + let lineWidth = 1.0 + UIScreenPixel + context.setLineWidth(lineWidth) + context.setStrokeColor(darkTheme.actionSheet.controlAccentColor.cgColor) + context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) + }) + } else { + return image + } + } + + items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { _, f in + f(.default) + + guard let strongSelf = self else { + return + } + + if peer.peer.id != myPeerId { + strongSelf.call.reconnect(as: peer.peer.id) + + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer.peer, text: strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false }) + } + }))) + + if peer.peer.id.namespace == Namespaces.Peer.CloudUser { + items.append(.separator) + } + } + if canManageCall { + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, action: { (c, _) in + guard let strongSelf = self else { + return + } + c.setItems(strongSelf.contextMenuMainItems()) + }))) + } + return items + } + } + + private func contextMenuPermissionItems() -> Signal<[ContextMenuItem], NoError> { + var items: [ContextMenuItem] = [] + if let callState = self.callState, callState.canManageCall, let defaultParticipantMuteState = callState.defaultParticipantMuteState { + let isMuted = defaultParticipantMuteState == .muted + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in + if isMuted { + return nil + } else { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) + } + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: false) + }))) + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.VoiceChat_SpeakPermissionAdmin, icon: { theme in + if !isMuted { + return nil + } else { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor) + } + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: true) + }))) + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] (c, _) in + guard let strongSelf = self else { + return + } + c.setItems(strongSelf.contextMenuMainItems()) + }))) + } + return .single(items) + } override func didLoad() { super.didLoad() @@ -3322,7 +3361,7 @@ public final class VoiceChatController: ViewController { self.reclaimActionButton = { [weak self, weak overlayController] in if let strongSelf = self { - overlayController?.animateOut(reclaim: true, completion: { [weak self] immediate in + overlayController?.animateOut(reclaim: true, completion: { immediate in if let strongSelf = self, immediate { strongSelf.controllerNode.actionButton.ignoreHierarchyChanges = true strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.actionButton) diff --git a/submodules/TelegramCore/Sources/GroupCalls.swift b/submodules/TelegramCore/Sources/GroupCalls.swift index 03cab25b8e..42846bafa1 100644 --- a/submodules/TelegramCore/Sources/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/GroupCalls.swift @@ -356,7 +356,7 @@ public struct JoinGroupCallResult { public var connectionMode: ConnectionMode } -public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, callId: Int64, accessHash: Int64, preferMuted: Bool, joinPayload: String, inviteHash: String? = nil) -> Signal { +public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, callId: Int64, accessHash: Int64, preferMuted: Bool, joinPayload: String, peerAdminIds: Signal<[PeerId], NoError>, inviteHash: String? = nil) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in if let joinAs = joinAs { return transaction.getPeer(joinAs).flatMap(apiInputPeer) @@ -377,8 +377,8 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal if let _ = inviteHash { flags |= (1 << 1) } - - return account.network.request(Api.functions.phone.joinGroupCall(flags: flags, call: .inputGroupCall(id: callId, accessHash: accessHash), joinAs: inputJoinAs, inviteHash: inviteHash, params: .dataJSON(data: joinPayload))) + + let joinRequest = account.network.request(Api.functions.phone.joinGroupCall(flags: flags, call: .inputGroupCall(id: callId, accessHash: accessHash), joinAs: inputJoinAs, inviteHash: inviteHash, params: .dataJSON(data: joinPayload))) |> mapError { error -> JoinGroupCallError in if error.errorDescription == "GROUPCALL_ANONYMOUS_FORBIDDEN" { return .anonymousNotAllowed @@ -387,92 +387,32 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal } return .generic } - |> mapToSignal { updates -> Signal in - let admins: Signal<(Set, [Api.User]), JoinGroupCallError> - if peerId.namespace == Namespaces.Peer.CloudChannel { - admins = account.postbox.transaction { transaction -> Api.InputChannel? in - return transaction.getPeer(peerId).flatMap(apiInputChannel) - } - |> castError(JoinGroupCallError.self) - |> mapToSignal { inputChannel -> Signal in - guard let inputChannel = inputChannel else { - return .fail(.generic) - } - - return account.network.request(Api.functions.channels.getParticipants(channel: inputChannel, filter: .channelParticipantsAdmins, offset: 0, limit: 100, hash: 0)) - |> `catch` { _ in - return .single(.channelParticipantsNotModified) - } - } - |> map { admins -> (Set, [Api.User]) in - var adminIds = Set() - var apiUsers: [Api.User] = [] - - switch admins { - case let .channelParticipants(_, participants, users): - apiUsers.append(contentsOf: users) - - for participant in participants { - let parsedParticipant = ChannelParticipant(apiParticipant: participant) - switch parsedParticipant { - case .creator: - adminIds.insert(parsedParticipant.peerId) - case let .member(_, _, adminInfo, _, _): - if let adminInfo = adminInfo, adminInfo.rights.rights.contains(.canManageCalls) { - adminIds.insert(parsedParticipant.peerId) - } - } - } - default: - break - } - - return (adminIds, apiUsers) - } - } else if peerId.namespace == Namespaces.Peer.CloudGroup { - admins = account.postbox.transaction { transaction -> (Set, [Api.User]) in - var result = Set() - if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData { - if let participants = cachedData.participants { - for participant in participants.participants { - if case .creator = participant { - result.insert(participant.peerId) - } else if case .admin = participant { - result.insert(participant.peerId) - } - } - } - } - return (result, []) - } - |> castError(JoinGroupCallError.self) - } else { - admins = .fail(.generic) - } - + + let getParticipantsRequest = getGroupCallParticipants(account: account, callId: callId, accessHash: accessHash, offset: "", ssrcs: [], limit: 100) + |> mapError { _ -> JoinGroupCallError in + return .generic + } + + return combineLatest( + joinRequest, + getParticipantsRequest + ) + |> mapToSignal { updates, participantsState -> Signal in let peer = account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) } |> castError(JoinGroupCallError.self) return combineLatest( - account.network.request(Api.functions.phone.getGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash))) - |> mapError { _ -> JoinGroupCallError in - return .generic - }, - getGroupCallParticipants(account: account, callId: callId, accessHash: accessHash, offset: "", ssrcs: [], limit: 100) - |> mapError { _ -> JoinGroupCallError in - return .generic - }, - admins, + peerAdminIds |> castError(JoinGroupCallError.self) |> take(1), peer ) - |> mapToSignal { result, state, admins, peer -> Signal in + |> mapToSignal { peerAdminIds, peer -> Signal in guard let peer = peer else { return .fail(.generic) } - var state = state + var state = participantsState if let channel = peer as? TelegramChannel { state.isCreator = channel.flags.contains(.isCreator) } else if let group = peer as? TelegramGroup { @@ -514,71 +454,53 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal var apiUsers: [Api.User] = [] - let (adminIds, adminUsers) = admins - apiUsers.append(contentsOf: adminUsers) - - state.adminIds = adminIds - - switch result { - case let .groupCall(call, _, _, chats, users): - guard let _ = GroupCallInfo(call) else { - return .fail(.generic) + state.adminIds = Set(peerAdminIds) + + var peers: [Peer] = [] + var peerPresences: [PeerId: PeerPresence] = [:] + + for user in apiUsers { + let telegramUser = TelegramUser(user: user) + peers.append(telegramUser) + if let presence = TelegramUserPresence(apiUser: user) { + peerPresences[telegramUser.id] = presence } - - apiUsers.append(contentsOf: users) - - var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] - - for user in apiUsers { - let telegramUser = TelegramUser(user: user) - peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } - } - - for chat in chats { - if let peer = parseTelegramGroupOrChannel(chat: chat) { - peers.append(peer) - } - } - - let connectionMode: JoinGroupCallResult.ConnectionMode - if let clientParams = parsedCall.clientParams, let clientParamsData = clientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: clientParamsData, options: [])) as? [String: Any] { - if let stream = dict["stream"] as? Bool, stream { - connectionMode = .broadcast - } else { - connectionMode = .rtc - } - } else { - connectionMode = .broadcast - } - - return account.postbox.transaction { transaction -> JoinGroupCallResult in - transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in - if let cachedData = cachedData as? CachedChannelData { - return cachedData.withUpdatedCallJoinPeerId(joinAs) - } else if let cachedData = cachedData as? CachedGroupData { - return cachedData.withUpdatedCallJoinPeerId(joinAs) - } else { - return cachedData - } - }) - - updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in - return updated - }) - updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) - - return JoinGroupCallResult( - callInfo: parsedCall, - state: state, - connectionMode: connectionMode - ) - } - |> castError(JoinGroupCallError.self) } + + let connectionMode: JoinGroupCallResult.ConnectionMode + if let clientParams = parsedCall.clientParams, let clientParamsData = clientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: clientParamsData, options: [])) as? [String: Any] { + if let stream = dict["stream"] as? Bool, stream { + connectionMode = .broadcast + } else { + connectionMode = .rtc + } + } else { + connectionMode = .broadcast + } + + return account.postbox.transaction { transaction -> JoinGroupCallResult in + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in + if let cachedData = cachedData as? CachedChannelData { + return cachedData.withUpdatedCallJoinPeerId(joinAs) + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.withUpdatedCallJoinPeerId(joinAs) + } else { + return cachedData + } + }) + + updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in + return updated + }) + updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences) + + return JoinGroupCallResult( + callInfo: parsedCall, + state: state, + connectionMode: connectionMode + ) + } + |> castError(JoinGroupCallError.self) } } } diff --git a/submodules/TelegramCore/Sources/TelegramUser.swift b/submodules/TelegramCore/Sources/TelegramUser.swift index ede6ff0844..c4225bb235 100644 --- a/submodules/TelegramCore/Sources/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/TelegramUser.swift @@ -87,7 +87,15 @@ extension TelegramUser { if !isMin { return TelegramUser(user: rhs) } else { - let telegramPhoto = photo.flatMap(parsedTelegramProfilePhoto) ?? [] + let telegramPhoto: [TelegramMediaImageRepresentation] + if let photo = photo { + telegramPhoto = parsedTelegramProfilePhoto(photo) + } else if let currentPhoto = lhs?.photo { + telegramPhoto = currentPhoto + } else { + telegramPhoto = [] + } + if let lhs = lhs { var userFlags: UserInfoFlags = [] if (flags & (1 << 17)) != 0 { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index e52bfb4ce1..2dba2806ee 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -465,9 +465,15 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } case let .joinVoiceChat(peerId, invite): dismissInput() - openPeer(peerId, defaultNavigationForPeerId(peerId, navigation: .default)) - present(VoiceChatJoinScreen(context: context, peerId: peerId, invite: invite, join: { call in - joinVoiceChat?(peerId, invite, call) - }), nil) + if let navigationController = navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), completion: { chatController in + guard let chatController = chatController as? ChatControllerImpl else { + return + } + navigationController.currentWindow?.present(VoiceChatJoinScreen(context: context, peerId: peerId, invite: invite, join: { [weak chatController] call in + chatController?.joinGroupCall(peerId: peerId, invite: invite, activeCall: call) + }), on: .root, blockInteraction: false, completion: {}) + })) + } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index 3a605ea78e..3d9378ce6d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -1039,9 +1039,9 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro if channel.flags.contains(.hasVoiceChat) { hasVoiceChat = true } - if !hasVoiceChat && (channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls)) { - canStartVoiceChat = true - } + } + if !hasVoiceChat && (channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls)) { + canStartVoiceChat = true } switch channel.participationStatus { case .member: diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 4a95374737..30070e3d27 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 4a953747375b8648f8b66e9572b59b10f7b769a1 +Subproject commit 30070e3d277debf4a69e0df001faffe571465614