diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index cc04aaffe3..1b00f4fe9f 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -683,6 +683,10 @@ private final class ConferenceCallE2EContextStateImpl: ConferenceCallE2EContextS return self.call.emojiState() } + func getParticipantIds() -> [Int64] { + return self.call.participantIds().compactMap { $0.int64Value } + } + func applyBlock(block: Data) { self.call.applyBlock(block) } @@ -691,6 +695,10 @@ private final class ConferenceCallE2EContextStateImpl: ConferenceCallE2EContextS self.call.applyBroadcastBlock(block) } + func generateRemoveParticipantsBlock(participantIds: [Int64]) -> Data? { + return self.call.generateRemoveParticipantsBlock(participantIds.map { $0 as NSNumber }) + } + func takeOutgoingBroadcastBlocks() -> [Data] { return self.call.takeOutgoingBroadcastBlocks() } @@ -1336,6 +1344,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { if case let .established(_, _, _, ssrc, _) = self.internalState, ssrc == participantUpdate.ssrc { self.markAsCanBeRemoved() } + } else { + self.e2eContext?.synchronizeRemovedParticipants() } } else if participantUpdate.peerId == self.joinAsPeerId { if case let .established(_, connectionMode, _, ssrc, _) = self.internalState { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index ae40c74c4e..2ba4db0dc0 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -356,7 +356,7 @@ extension VideoChatScreenComponent.View { }))) } - if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { + if case let .group(groupCall) = currentCall, !groupCall.isConference, callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) { if currentCall.hasScreencast { items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_StopScreenSharing, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor) diff --git a/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift b/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift index b513fb0dfc..e5786d428f 100644 --- a/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift +++ b/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift @@ -3,9 +3,12 @@ import SwiftSignalKit public protocol ConferenceCallE2EContextState: AnyObject { func getEmojiState() -> Data? + func getParticipantIds() -> [Int64] func applyBlock(block: Data) func applyBroadcastBlock(block: Data) + + func generateRemoveParticipantsBlock(participantIds: [Int64]) -> Data? func takeOutgoingBroadcastBlocks() -> [Data] @@ -43,6 +46,12 @@ public final class ConferenceCallE2EContext { private var e2ePoll1Timer: Foundation.Timer? private var e2ePoll1Disposable: Disposable? + private var isSynchronizingRemovedParticipants: Bool = false + private var scheduledSynchronizeRemovedParticipants: Bool = false + private var scheduledSynchronizeRemovedParticipantsAfterPoll: Bool = false + private var synchronizeRemovedParticipantsDisposable: Disposable? + private var synchronizeRemovedParticipantsTimer: Foundation.Timer? + init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64, reference: InternalGroupCallReference, state: Atomic, initializeState: @escaping (TelegramKeyPair, Data) -> ConferenceCallE2EContextState?, keyPair: TelegramKeyPair) { precondition(queue.isCurrent()) precondition(Queue.mainQueue().isCurrent()) @@ -62,9 +71,19 @@ public final class ConferenceCallE2EContext { self.e2ePoll0Disposable?.dispose() self.e2ePoll1Timer?.invalidate() self.e2ePoll1Disposable?.dispose() + self.synchronizeRemovedParticipantsDisposable?.dispose() + self.synchronizeRemovedParticipantsTimer?.invalidate() } func begin() { + self.scheduledSynchronizeRemovedParticipantsAfterPoll = true + self.synchronizeRemovedParticipantsTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.synchronizeRemovedParticipants() + }) + self.e2ePoll(subChainId: 0) self.e2ePoll(subChainId: 1) } @@ -189,6 +208,11 @@ public final class ConferenceCallE2EContext { } self.e2ePoll(subChainId: 0) }) + + if self.scheduledSynchronizeRemovedParticipantsAfterPoll { + self.scheduledSynchronizeRemovedParticipantsAfterPoll = false + self.synchronizeRemovedParticipants() + } } else if subChainId == 1 { self.e2ePoll1Timer?.invalidate() self.e2ePoll1Timer = Foundation.Timer.scheduledTimer(withTimeInterval: delayPoll ? 1.0 : 0.0, repeats: false, block: { [weak self] _ in @@ -208,7 +232,83 @@ public final class ConferenceCallE2EContext { } func synchronizeRemovedParticipants() { + if self.isSynchronizingRemovedParticipants { + self.scheduledSynchronizeRemovedParticipants = true + return + } + + self.isSynchronizingRemovedParticipants = true + + let engine = self.engine + let state = self.state + let callId = self.callId + let accessHash = self.accessHash + self.synchronizeRemovedParticipantsDisposable?.dispose() + self.synchronizeRemovedParticipantsDisposable = (_internal_getGroupCallParticipants( + account: self.engine.account, + reference: self.reference, + offset: "", + ssrcs: [], + limit: 100, + sortAscending: true + ) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .single(false) + } + + let blockchainPeerIds = state.with { state -> [Int64] in + guard let state = state.state else { + return [] + } + return state.getParticipantIds() + } + + // 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 }) + } + + if removedPeerIds.isEmpty { + return .single(false) + } + guard let removeBlock = state.with({ state -> Data? in + guard let state = state.state else { + return nil + } + return state.generateRemoveParticipantsBlock(participantIds: removedPeerIds) + }) else { + return .single(false) + } + + return engine.calls.removeGroupCallBlockchainParticipants(callId: callId, accessHash: accessHash, participantIds: removedPeerIds, block: removeBlock) + |> map { result -> Bool in + switch result { + case .success: + return true + case .pollBlocksAndRetry: + return false + } + } + } + |> deliverOn(self.queue)).startStrict(next: { [weak self] shouldRetry in + guard let self else { + return + } + self.isSynchronizingRemovedParticipants = false + if self.scheduledSynchronizeRemovedParticipants { + self.scheduledSynchronizeRemovedParticipants = false + self.synchronizeRemovedParticipants() + } else if shouldRetry && !self.scheduledSynchronizeRemovedParticipantsAfterPoll { + self.scheduledSynchronizeRemovedParticipantsAfterPoll = true + self.e2ePoll(subChainId: 0) + } + }) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index d9cb5160fd..393a1be525 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -854,54 +854,32 @@ func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64, } } -public enum RemoveGroupCallBlockchainParticipantError { - case generic +public enum RemoveGroupCallBlockchainParticipantsResult { + case success case pollBlocksAndRetry } -func _internal_removeGroupCallBlockchainParticipants(account: Account, callId: Int64, accessHash: Int64, block: @escaping ([EnginePeer.Id]) -> Data?) -> Signal { - /*let blockSignal = _internal_getGroupCallParticipants(account: account, callId: callId, accessHash: accessHash, offset: "", ssrcs: [], limit: 1000, sortAscending: nil) - |> mapError { _ -> RemoveGroupCallBlockchainParticipantError in - return .generic - } - |> map { result -> Data? in - return block(result.participants.map(\.peer.id)) - } - - let signal: Signal = blockSignal - |> mapToSignal { block -> Signal in - guard let block else { - return .complete() - } - return - } - - account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(participantId).flatMap(apiInputPeer) - } - |> castError(RemoveGroupCallBlockchainParticipantError.self) - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer else { - return .fail(.generic) - } - return account.network.request(Api.functions.phone.deleteConferenceCallParticipant(call: .inputGroupCall(id: callId, accessHash: accessHash), peer: inputPeer, block: Buffer(data: block))) - |> mapError { error -> RemoveGroupCallBlockchainParticipantError in - if error.errorDescription.hasPrefix("CONF_WRITE_CHAIN_INVALID") { - return .pollBlocksAndRetry +func _internal_removeGroupCallBlockchainParticipants(account: Account, callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal { + return account.postbox.transaction { transaction -> [Api.InputPeer] in + return participantIds.map { participantId in + let participantPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(participantId)) + if let peer = transaction.getPeer(participantPeerId).flatMap(apiInputPeer) { + return peer } else { - return .generic + return .inputPeerUser(userId: participantId, accessHash: 0) } } - |> mapToSignal { result -> Signal in - account.stateManager.addUpdates(result) - - return .complete() + } + |> mapToSignal { inputUsers -> Signal in + return account.network.request(Api.functions.phone.deleteConferenceCallParticipants(call: .inputGroupCall(id: callId, accessHash: accessHash), ids: inputUsers, block: Buffer(data: block))) + |> map { updates -> RemoveGroupCallBlockchainParticipantsResult in + account.stateManager.addUpdates(updates) + return .success + } + |> `catch` { _ -> Signal in + return .single(.pollBlocksAndRetry) } } - - return signal*/ - //TODO:release - return .complete() } public struct JoinGroupCallAsScreencastResult { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift index 2e04767276..c5e06f565d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift @@ -109,8 +109,8 @@ public extension TelegramEngine { return _internal_inviteConferenceCallParticipant(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId) } - public func removeGroupCallBlockchainParticipant(callId: Int64, accessHash: Int64, block: @escaping ([EnginePeer.Id]) -> Data?) -> Signal { - return _internal_removeGroupCallBlockchainParticipants(account: self.account, callId: callId, accessHash: accessHash, block: block) + public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal { + return _internal_removeGroupCallBlockchainParticipants(account: self.account, callId: callId, accessHash: accessHash, participantIds: participantIds, block: block) } public func clearCachedGroupCallDisplayAsAvailablePeers(peerId: PeerId) -> Signal { diff --git a/third-party/td/TdBinding/Public/TdBinding/TdBinding.h b/third-party/td/TdBinding/Public/TdBinding/TdBinding.h index d2c6f8384d..bf94256c05 100644 --- a/third-party/td/TdBinding/Public/TdBinding/TdBinding.h +++ b/third-party/td/TdBinding/Public/TdBinding/TdBinding.h @@ -35,10 +35,13 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *)takeOutgoingBroadcastBlocks; - (NSData *)emojiState; +- (NSArray *)participantIds; - (void)applyBlock:(NSData *)block; - (void)applyBroadcastBlock:(NSData *)block; +- (nullable NSData *)generateRemoveParticipantsBlock:(NSArray *)participantIds; + - (nullable NSData *)encrypt:(NSData *)message; - (nullable NSData *)decrypt:(NSData *)message; diff --git a/third-party/td/TdBinding/Sources/TdBinding.mm b/third-party/td/TdBinding/Sources/TdBinding.mm index ad35d5ea00..90d4e91c58 100644 --- a/third-party/td/TdBinding/Sources/TdBinding.mm +++ b/third-party/td/TdBinding/Sources/TdBinding.mm @@ -159,6 +159,19 @@ static NSString *hexStringFromData(NSData *data) { return outEmojiHash; } +- (NSArray *)participantIds { + auto result = tde2e_api::call_get_state(_callId); + if (!result.is_ok()) { + return @[]; + } + auto state = result.value(); + NSMutableArray *participantIds = [[NSMutableArray alloc] init]; + for (const auto &it : state.participants) { + [participantIds addObject:[NSNumber numberWithLongLong:it.user_id]]; + } + return participantIds; +} + - (void)applyBlock:(NSData *)block { std::string mappedBlock((uint8_t *)block.bytes, ((uint8_t *)block.bytes) + block.length); @@ -195,6 +208,28 @@ static NSString *hexStringFromData(NSData *data) { } } +- (nullable NSData *)generateRemoveParticipantsBlock:(NSArray *)participantIds { + auto stateResult = tde2e_api::call_get_state(_callId); + if (!stateResult.is_ok()) { + return nil; + } + auto state = stateResult.value(); + for (NSNumber *participantId in participantIds) { + auto it = std::find_if(state.participants.begin(), state.participants.end(), [participantId](const tde2e_api::CallParticipant &participant) { + return participant.user_id == [participantId longLongValue]; + }); + if (it != state.participants.end()) { + state.participants.erase(it); + } + } + + auto result = tde2e_api::call_create_change_state_block(_callId, state); + if (!result.is_ok()) { + return nil; + } + return [[NSData alloc] initWithBytes:result.value().data() length:result.value().size()]; +} + - (nullable NSData *)encrypt:(NSData *)message { std::string mappedMessage((uint8_t *)message.bytes, ((uint8_t *)message.bytes) + message.length); auto result = tde2e_api::call_encrypt(_callId, mappedMessage);