From e8b7f53c84429bcbc43436639da996fb44f3dd2e Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 4 Apr 2025 14:04:52 +0400 Subject: [PATCH 1/2] Conference updates --- .../VideoChatListInviteComponent.swift | 28 +++- .../VideoChatParticipantsComponent.swift | 152 ++++++++++-------- .../Sources/State/AccountViewTracker.swift | 64 ++++++++ .../TelegramEngine/Calls/GroupCalls.swift | 103 ++++++++++++ .../ChatMessageCallBubbleContentNode.swift | 4 +- .../Sources/ChatHistoryListNode.swift | 16 ++ 6 files changed, 297 insertions(+), 70 deletions(-) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift index 8e089cec0f..e84a5aac38 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatListInviteComponent.swift @@ -15,17 +15,20 @@ final class VideoChatListInviteComponent: Component { let title: String let icon: Icon let theme: PresentationTheme + let hasNext: Bool let action: () -> Void init( title: String, icon: Icon, theme: PresentationTheme, + hasNext: Bool, action: @escaping () -> Void ) { self.title = title self.icon = icon self.theme = theme + self.hasNext = hasNext self.action = action } @@ -39,6 +42,9 @@ final class VideoChatListInviteComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.hasNext != rhs.hasNext { + return false + } return true } @@ -52,7 +58,11 @@ final class VideoChatListInviteComponent: Component { private var highlightBackgroundLayer: SimpleLayer? private var highlightBackgroundFrame: CGRect? + private let separatorLayer: SimpleLayer + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + super.init(frame: frame) self.highligthedChanged = { [weak self] isHighlighted in @@ -74,6 +84,15 @@ final class VideoChatListInviteComponent: Component { } highlightBackgroundLayer.frame = highlightBackgroundFrame highlightBackgroundLayer.opacity = 1.0 + if component.hasNext { + highlightBackgroundLayer.maskedCorners = [] + highlightBackgroundLayer.masksToBounds = false + highlightBackgroundLayer.cornerRadius = 0.0 + } else { + highlightBackgroundLayer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + highlightBackgroundLayer.masksToBounds = true + highlightBackgroundLayer.cornerRadius = 10.0 + } } else { if let highlightBackgroundLayer = self.highlightBackgroundLayer { self.highlightBackgroundLayer = nil @@ -152,7 +171,14 @@ final class VideoChatListInviteComponent: Component { transition.setFrame(view: iconView, frame: iconFrame) } - //self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: size) + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: size) + + if self.separatorLayer.superlayer == nil { + self.layer.addSublayer(self.separatorLayer) + } + self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 62.0, y: size.height), size: CGSize(width: size.width - 62.0, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext return size } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index a8ce722e88..242d67f9ea 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -1760,74 +1760,6 @@ final class VideoChatParticipantsComponent: Component { } } - let measureListItemSize = self.measureListItemView.update( - transition: .immediate, - component: AnyComponent(PeerListItemComponent( - context: component.call.accountContext, - theme: component.theme, - strings: component.strings, - style: .generic, - sideInset: 0.0, - title: "AAA", - peer: nil, - subtitle: PeerListItemComponent.Subtitle(text: "bbb", color: .neutral), - subtitleAccessory: .none, - presence: nil, - selectionState: .none, - hasNext: true, - action: { _, _, _ in - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 1000.0) - ) - - var inviteListItemSizes: [CGSize] = [] - for (inviteOption) in component.participants?.inviteOptions ?? [] { - let inviteText: String - let iconType: VideoChatListInviteComponent.Icon - switch inviteOption.type { - case let .invite(isMultiple): - //TODO:localize - if isMultiple { - inviteText = component.strings.VoiceChat_InviteMember - } else { - inviteText = "Add Member" - } - iconType = .addUser - case .shareLink: - inviteText = component.strings.VoiceChat_Share - iconType = .link - } - - let inviteListItemView: ComponentView - var inviteListItemTransition = transition - if let current = self.inviteListItemViews[inviteOption.id] { - inviteListItemView = current - } else { - inviteListItemView = ComponentView() - self.inviteListItemViews[inviteOption.id] = inviteListItemView - inviteListItemTransition = inviteListItemTransition.withAnimation(.none) - } - - inviteListItemSizes.append(inviteListItemView.update( - transition: inviteListItemTransition, - component: AnyComponent(VideoChatListInviteComponent( - title: inviteText, - icon: iconType, - theme: component.theme, - action: { [weak self] in - guard let self, let component = self.component else { - return - } - component.openInviteMembers(inviteOption.type) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 1000.0) - )) - } - var gridParticipants: [VideoParticipant] = [] var listParticipants: [GroupCallParticipantsContext.Participant] = [] if let participants = component.participants { @@ -1868,6 +1800,90 @@ final class VideoChatParticipantsComponent: Component { self.gridParticipants = gridParticipants self.listParticipants = listParticipants + let measureListItemSize = self.measureListItemView.update( + transition: .immediate, + component: AnyComponent(PeerListItemComponent( + context: component.call.accountContext, + theme: component.theme, + strings: component.strings, + style: .generic, + sideInset: 0.0, + title: "AAA", + peer: nil, + subtitle: PeerListItemComponent.Subtitle(text: "bbb", color: .neutral), + subtitleAccessory: .none, + presence: nil, + selectionState: .none, + hasNext: true, + action: { _, _, _ in + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + + var inviteListItemSizes: [CGSize] = [] + if let participants = component.participants { + let tempItemLayout = ItemLayout( + containerSize: availableSize, + layout: component.layout, + isUIHidden: component.expandedVideoState?.isUIHidden ?? false, + expandedInsets: component.expandedInsets, + safeInsets: component.safeInsets, + gridItemCount: gridParticipants.count, + listItemCount: listParticipants.count + component.invitedPeers.count, + listItemHeight: measureListItemSize.height, + listTrailingItemHeights: [] + ) + + for i in 0 ..< participants.inviteOptions.count { + let inviteOption = participants.inviteOptions[i] + let inviteText: String + let iconType: VideoChatListInviteComponent.Icon + switch inviteOption.type { + case let .invite(isMultiple): + //TODO:localize + if isMultiple { + inviteText = component.strings.VoiceChat_InviteMember + } else { + inviteText = "Add Member" + } + iconType = .addUser + case .shareLink: + inviteText = component.strings.VoiceChat_Share + iconType = .link + } + + let inviteListItemView: ComponentView + var inviteListItemTransition = transition + if let current = self.inviteListItemViews[inviteOption.id] { + inviteListItemView = current + } else { + inviteListItemView = ComponentView() + self.inviteListItemViews[inviteOption.id] = inviteListItemView + inviteListItemTransition = inviteListItemTransition.withAnimation(.none) + } + + inviteListItemSizes.append(inviteListItemView.update( + transition: inviteListItemTransition, + component: AnyComponent(VideoChatListInviteComponent( + title: inviteText, + icon: iconType, + theme: component.theme, + hasNext: i != participants.inviteOptions.count - 1, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.openInviteMembers(inviteOption.type) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - tempItemLayout.list.sideInset * 2.0, height: 1000.0) + )) + } + } + let itemLayout = ItemLayout( containerSize: availableSize, layout: component.layout, diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 8d2bcdfb15..9923e728e1 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -1301,6 +1301,70 @@ public final class AccountViewTracker { } } + public func refreshInlineGroupCallsForMessageIds(messageIds: Set) { + self.queue.async { + var addedMessageIds: [MessageId] = [] + let timestamp = Int32(CFAbsoluteTimeGetCurrent()) + for messageId in messageIds { + let messageTimestamp = self.updatedUnsupportedMediaMessageIdsAndTimestamps[MessageAndThreadId(messageId: messageId, threadId: nil)] + var refresh = false + if let messageTimestamp = messageTimestamp { + refresh = messageTimestamp < timestamp - 60 + } else { + refresh = true + } + + if refresh { + self.updatedUnsupportedMediaMessageIdsAndTimestamps[MessageAndThreadId(messageId: messageId, threadId: nil)] = timestamp + addedMessageIds.append(messageId) + } + } + if !addedMessageIds.isEmpty { + for (_, messageIds) in messagesIdsGroupedByPeerId(Set(addedMessageIds)) { + let disposableId = self.nextUpdatedUnsupportedMediaDisposableId + self.nextUpdatedUnsupportedMediaDisposableId += 1 + + if let account = self.account { + let signal = account.postbox.transaction { transaction -> [MessageId] in + var result: [MessageId] = [] + for id in messageIds { + if let message = transaction.getMessage(id) { + for media in message.media { + if let _ = media as? TelegramMediaAction { + result.append(id) + break + } + } + } + } + return result + } + |> mapToSignal { ids -> Signal in + guard !ids.isEmpty else { + return .complete() + } + + var requests: [Signal] = [] + + for id in ids { + requests.append(_internal_refreshInlineGroupCall(account: account, messageId: id)) + } + + return combineLatest(requests) + |> ignoreValues + } + |> afterDisposed { [weak self] in + self?.queue.async { + self?.updatedUnsupportedMediaDisposables.set(nil, forKey: disposableId) + } + } + self.updatedUnsupportedMediaDisposables.set(signal.start(), forKey: disposableId) + } + } + } + } + } + public func refreshStoryStatsForPeerIds(peerIds: [PeerId]) { self.queue.async { self.pendingRefreshStoriesForPeerIds.append(contentsOf: peerIds) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index b4df402821..af3a013edb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -241,6 +241,53 @@ func _internal_getCurrentGroupCall(account: Account, reference: InternalGroupCal } } +func _internal_getCurrentGroupCallInfo(account: Account, reference: InternalGroupCallReference) -> Signal<(participants: [PeerId], duration: Int32?)?, NoError> { + let accountPeerId = account.peerId + let inputCall: Api.InputGroupCall + switch reference { + case let .id(id, accessHash): + inputCall = .inputGroupCall(id: id, accessHash: accessHash) + case let .link(slug): + inputCall = .inputGroupCallSlug(slug: slug) + case let .message(id): + if id.peerId.namespace != Namespaces.Peer.CloudUser { + return .single(nil) + } + if id.namespace != Namespaces.Message.Cloud { + return .single(nil) + } + inputCall = .inputGroupCallInviteMessage(msgId: id.id) + } + return account.network.request(Api.functions.phone.getGroupCall(call: inputCall, limit: 4)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<(participants: [PeerId], duration: Int32?)?, NoError> in + guard let result else { + return .single(nil) + } + switch result { + case let .groupCall(call, participants, _, chats, users): + return account.postbox.transaction { transaction -> (participants: [PeerId], duration: Int32?)? in + if case let .groupCallDiscarded(_, _, duration) = call { + return ([], duration) + } + + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) + + let parsedParticipants = participants.compactMap { GroupCallParticipantsContext.Participant($0, transaction: transaction) } + return ( + parsedParticipants.map(\.peer.id), + nil + ) + } + } + } +} + public enum CreateGroupCallError { case generic case anonymousNotAllowed @@ -3050,3 +3097,59 @@ func _internal_sendConferenceCallBroadcast(account: Account, callId: Int64, acce return .complete() } } + +func _internal_refreshInlineGroupCall(account: Account, messageId: MessageId) -> Signal { + return _internal_getCurrentGroupCallInfo(account: account, reference: .message(id: messageId)) + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> Void in + transaction.updateMessage(messageId, update: { currentMessage in + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) + } + var updatedMedia = currentMessage.media + + for i in 0 ..< updatedMedia.count { + if let action = updatedMedia[i] as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action { + var otherParticipants: [PeerId] = [] + var duration: Int32? = conferenceCall.duration + if let result { + for id in result.participants { + if id != account.peerId { + otherParticipants.append(id) + } + } + duration = result.duration + } else { + duration = nil + } + + updatedMedia[i] = TelegramMediaAction(action: .conferenceCall(TelegramMediaActionType.ConferenceCall( + callId: conferenceCall.callId, + duration: duration, + flags: conferenceCall.flags, + otherParticipants: otherParticipants + ))) + } + } + return .update(StoreMessage( + id: currentMessage.id, + globallyUniqueId: currentMessage.globallyUniqueId, + groupingKey: currentMessage.groupingKey, + threadId: currentMessage.threadId, + timestamp: currentMessage.timestamp, + flags: StoreMessageFlags(currentMessage.flags), + tags: currentMessage.tags, + globalTags: currentMessage.globalTags, + localTags: currentMessage.localTags, + forwardInfo: storeForwardInfo, + authorId: currentMessage.author?.id, + text: currentMessage.text, + attributes: currentMessage.attributes, + media: updatedMedia + )) + }) + } + |> ignoreValues + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift index 75987f4d84..38d85daf9c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift @@ -142,6 +142,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { #else missedTimeout = 30 #endif + if conferenceCall.duration != nil { + hasCallButton = false + } let currentTime = Int32(Date().timeIntervalSince1970) if conferenceCall.flags.contains(.isMissed) { titleString = "Declined Group Call" @@ -149,7 +152,6 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { titleString = "Missed Group Call" } else if conferenceCall.duration != nil { titleString = "Cancelled Group Call" - hasCallButton = true } else { if incoming { titleString = "Incoming Group Call" diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index cd88b36aa2..1a45674174 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -586,6 +586,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto private let translationProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0) private let refreshStoriesProcessingManager = ChatMessageThrottledProcessingManager() private let factCheckProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0) + private let inlineGroupCallsProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0) let prefetchManager: InChatPrefetchManager private var currentEarlierPrefetchMessages: [(Message, Media)] = [] @@ -978,6 +979,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto strongSelf.context.account.viewTracker.updatedExtendedMediaForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } + self.inlineGroupCallsProcessingManager.process = { [weak context] messageIds in + context?.account.viewTracker.refreshInlineGroupCallsForMessageIds(messageIds: Set(messageIds.map(\.messageId))) + } + self.preloadPages = false self.beginChatHistoryTransitions(resetScrolling: false) @@ -2728,6 +2733,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var messageIdsWithUnseenPersonalMention: [MessageId] = [] var messageIdsWithUnseenReactions: [MessageId] = [] var messageIdsWithInactiveExtendedMedia = Set() + var messageIdsWithGroupCalls: [MessageId] = [] var downloadableResourceIds: [(messageId: MessageId, resourceId: String)] = [] var allVisibleAnchorMessageIds: [(MessageId, Int)] = [] var visibleAdOpaqueIds: [Data] = [] @@ -2826,6 +2832,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto storiesRequiredValidation = true } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let _ = content.story { storiesRequiredValidation = true + } else if let media = media as? TelegramMediaAction { + if case let .conferenceCall(conferenceCall) = media.action { + if conferenceCall.duration != nil { + } else { + messageIdsWithGroupCalls.append(message.id) + } + } } } if contentRequiredValidation { @@ -3083,6 +3096,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if !peerIdsWithRefreshStories.isEmpty { self.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: peerIdsWithRefreshStories) } + if !messageIdsWithGroupCalls.isEmpty { + self.inlineGroupCallsProcessingManager.add(messageIdsWithGroupCalls.map { MessageAndThreadId(messageId: $0, threadId: nil) }) + } self.currentEarlierPrefetchMessages = toEarlierMediaMessages self.currentLaterPrefetchMessages = toLaterMediaMessages From 0a499f4e2b171fee939137a7de5cdb54f11fb84c Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 4 Apr 2025 16:51:46 +0400 Subject: [PATCH 2/2] Conference updates --- .../Sources/NotificationService.swift | 1 - .../Sources/PresentationCallManager.swift | 1 + .../Sources/PresentationCall.swift | 8 + .../Sources/PresentationGroupCall.swift | 8 + .../VideoChatParticipantsComponent.swift | 17 +- .../Sources/VideoChatScreen.swift | 6 + ...ideoChatScreenParticipantContextMenu.swift | 62 +++- .../State/ConferenceCallE2EContext.swift | 268 ++++++++++++------ third-party/td/TdBinding/Sources/TdBinding.mm | 60 +++- third-party/td/build-td-bazel.sh | 1 - 10 files changed, 340 insertions(+), 92 deletions(-) diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index a3612b311f..87d3a46560 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -961,7 +961,6 @@ private final class NotificationServiceHandler { if let locKey = payloadJson["loc-key"] as? String, (locKey == "CONF_CALL_REQUEST" || locKey == "CONF_CALL_MISSED"), let callIdString = payloadJson["call_id"] as? String { if let callId = Int64(callIdString) { - if let updates = payloadJson["updates"] as? String { var updateString = updates updateString = updateString.replacingOccurrences(of: "-", with: "+") diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 790e705ab4..280a392754 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -487,6 +487,7 @@ public protocol PresentationGroupCall: AnyObject { func updateTitle(_ title: String) func invitePeer(_ peerId: EnginePeer.Id, isVideo: Bool) -> Bool + func kickPeer(id: EnginePeer.Id) func removedPeer(_ peerId: EnginePeer.Id) var invitedPeers: Signal<[PresentationGroupCallInvitedPeer], NoError> { get } diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 8d09ee2132..10a083f8b3 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -217,6 +217,12 @@ public final class SharedCallAudioContext { } } } + + public func switchToSpeakerIfBuiltin() { + if case .builtin = self.currentAudioOutputValue { + self.setCurrentAudioOutput(.speaker) + } + } } public final class PresentationCallImpl: PresentationCall { @@ -1032,6 +1038,8 @@ public final class PresentationCallImpl: PresentationCall { self.conferenceCallImpl = conferenceCall conferenceCall.upgradedConferenceCall = self + self.sharedAudioContext?.switchToSpeakerIfBuiltin() + for (peerId, isVideo) in self.pendingInviteToConferencePeerIds { let _ = conferenceCall.invitePeer(peerId, isVideo: isVideo) } diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 7e2325d5da..ce97d819e1 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -3986,6 +3986,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } + public func kickPeer(id: EnginePeer.Id) { + if self.isConference { + self.removedPeer(id) + + self.e2eContext?.kickPeer(id: id) + } + } + public func removedPeer(_ peerId: PeerId) { var updatedInvitedPeers = self.invitedPeersValue updatedInvitedPeers.removeAll(where: { $0.id == peerId}) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 242d67f9ea..879ebc665e 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -149,6 +149,7 @@ final class VideoChatParticipantsComponent: Component { let safeInsets: UIEdgeInsets let interfaceOrientation: UIInterfaceOrientation let openParticipantContextMenu: (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void + let openInvitedParticipantContextMenu: (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void let updateIsMainParticipantPinned: (Bool) -> Void let updateIsExpandedUIHidden: (Bool) -> Void @@ -169,6 +170,7 @@ final class VideoChatParticipantsComponent: Component { safeInsets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, openParticipantContextMenu: @escaping (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void, + openInvitedParticipantContextMenu: @escaping (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void, updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void, updateIsMainParticipantPinned: @escaping (Bool) -> Void, updateIsExpandedUIHidden: @escaping (Bool) -> Void, @@ -188,6 +190,7 @@ final class VideoChatParticipantsComponent: Component { self.safeInsets = safeInsets self.interfaceOrientation = interfaceOrientation self.openParticipantContextMenu = openParticipantContextMenu + self.openInvitedParticipantContextMenu = openInvitedParticipantContextMenu self.updateMainParticipant = updateMainParticipant self.updateIsMainParticipantPinned = updateIsMainParticipantPinned self.updateIsExpandedUIHidden = updateIsExpandedUIHidden @@ -1317,8 +1320,18 @@ final class VideoChatParticipantsComponent: Component { inset: 2.0, background: UIColor(white: 0.1, alpha: 1.0) ), - action: nil, - contextAction: nil + action: { [weak self] peer, _, itemView in + guard let self, let component = self.component else { + return + } + component.openInvitedParticipantContextMenu(peer.id, itemView.extractedContainerView, nil) + }, + contextAction: { [weak self] peer, sourceView, gesture in + guard let self, let component = self.component else { + return + } + component.openInvitedParticipantContextMenu(peer.id, sourceView, gesture) + } ) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 658e2beaa0..4fedb48475 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -2329,6 +2329,12 @@ final class VideoChatScreenComponent: Component { } self.openParticipantContextMenu(id: id, sourceView: sourceView, gesture: gesture) }, + openInvitedParticipantContextMenu: { [weak self] id, sourceView, gesture in + guard let self else { + return + } + self.openInvitedParticipantContextMenu(id: id, sourceView: sourceView, gesture: gesture) + }, updateMainParticipant: { [weak self] key, alsoSetIsUIHidden in guard let self else { return diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift index d34f216387..0161d1a73d 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -299,7 +299,11 @@ extension VideoChatScreenComponent.View { } let _ = groupCall.accountContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: groupCall.accountContext.engine, peerId: callPeerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start() - groupCall.removedPeer(peer.id) + if groupCall.isConference { + groupCall.kickPeer(id: peer.id) + } else { + groupCall.removedPeer(peer.id) + } self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(EnginePeer(peer).displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false }) })) @@ -347,6 +351,62 @@ extension VideoChatScreenComponent.View { environment.controller()?.presentInGlobalOverlay(contextController) } + func openInvitedParticipantContextMenu(id: EnginePeer.Id, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) { + guard let environment = self.environment else { + return + } + guard let currentCall = self.currentCall else { + return + } + guard case .group = self.currentCall else { + return + } + + let itemsForEntry: () -> [ContextMenuItem] = { [weak self] in + guard let self, let environment = self.environment else { + return [] + } + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: environment.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(result: .dismissWithoutContent, completion: nil) + + guard let self else { + return + } + guard case let .group(groupCall) = self.currentCall else { + return + } + + groupCall.kickPeer(id: id) + }))) + return items + } + + let items = itemsForEntry() + + let presentationData = currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + let contextController = ContextController( + presentationData: presentationData, + source: .extracted(ParticipantExtractedContentSource(contentView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + recognizer: nil, + gesture: gesture + ) + + environment.controller()?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + return true + }) + + environment.controller()?.presentInGlobalOverlay(contextController) + } + private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) { guard let currentCall = self.currentCall else { return diff --git a/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift b/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift index cb7aa51cb6..367753900a 100644 --- a/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift +++ b/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift @@ -52,6 +52,8 @@ public final class ConferenceCallE2EContext { private var scheduledSynchronizeRemovedParticipantsAfterPoll: Bool = false private var synchronizeRemovedParticipantsDisposable: Disposable? private var synchronizeRemovedParticipantsTimer: Foundation.Timer? + + private var pendingKickPeers: [EnginePeer.Id] = [] init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64, userId: Int64, reference: InternalGroupCallReference, state: Atomic, initializeState: @escaping (TelegramKeyPair, Int64, Data) -> ConferenceCallE2EContextState?, keyPair: TelegramKeyPair) { precondition(queue.isCurrent()) @@ -91,37 +93,53 @@ public final class ConferenceCallE2EContext { } func addChainBlocksUpdate(subChainId: Int, blocks: [Data], nextOffset: Int) { - var processBlock = true let updateBaseOffset = nextOffset - blocks.count - if subChainId == 0 { - if let e2ePoll0Offset = self.e2ePoll0Offset { - if e2ePoll0Offset == updateBaseOffset { - self.e2ePoll0Offset = nextOffset - } else if e2ePoll0Offset < updateBaseOffset { - self.e2ePoll(subChainId: subChainId) - } else { - processBlock = false + var blocksToProcess: [Data] = [] + var shouldPoll = false + for i in 0 ..< blocks.count { + let blockOffset = updateBaseOffset + i + if subChainId == 0 { + if var e2ePoll0Offset = self.e2ePoll0Offset { + if blockOffset == e2ePoll0Offset { + e2ePoll0Offset += 1 + self.e2ePoll0Offset = e2ePoll0Offset + blocksToProcess.append(blocks[i]) + } else if blockOffset > e2ePoll0Offset { + shouldPoll = true + } } - } else { - processBlock = false - } - } else if subChainId == 1 { - if let e2ePoll1Offset = self.e2ePoll1Offset { - if e2ePoll1Offset == updateBaseOffset { - self.e2ePoll1Offset = nextOffset - } else if e2ePoll1Offset < updateBaseOffset { - self.e2ePoll(subChainId: subChainId) - } else { - processBlock = false + } else if subChainId == 1 { + if var e2ePoll1Offset = self.e2ePoll1Offset { + if blockOffset == e2ePoll1Offset { + e2ePoll1Offset += 1 + self.e2ePoll1Offset = e2ePoll1Offset + blocksToProcess.append(blocks[i]) + } else if blockOffset > e2ePoll1Offset { + shouldPoll = true + } } - } else { - processBlock = false } - } else { - processBlock = false } - if processBlock { - self.addE2EBlocks(blocks: blocks, subChainId: subChainId) + + if !blocksToProcess.isEmpty { + if subChainId == 0 { + if self.e2ePoll0Disposable != nil { + self.e2ePoll0Disposable?.dispose() + self.e2ePoll0Disposable = nil + shouldPoll = true + } + } else if subChainId == 1 { + if self.e2ePoll1Disposable != nil { + self.e2ePoll1Disposable?.dispose() + self.e2ePoll1Disposable = nil + shouldPoll = true + } + } + self.addE2EBlocks(blocks: blocksToProcess, subChainId: subChainId) + } + + if shouldPoll { + self.e2ePoll(subChainId: subChainId) } } @@ -186,6 +204,14 @@ public final class ConferenceCallE2EContext { guard let self else { return } + + if subChainId == 0 { + self.e2ePoll0Disposable?.dispose() + self.e2ePoll0Disposable = nil + } else if subChainId == 1 { + self.e2ePoll1Disposable?.dispose() + self.e2ePoll1Disposable = nil + } var delayPoll = true if let result { @@ -247,71 +273,139 @@ public final class ConferenceCallE2EContext { 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 !self.pendingKickPeers.isEmpty { + let pendingKickPeers = self.pendingKickPeers + self.pendingKickPeers.removeAll() - if removedPeerIds.isEmpty { - return .single(false) - } - guard let removeBlock = state.with({ state -> Data? in + self.synchronizeRemovedParticipantsDisposable?.dispose() + + 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, mode: .cleanup, participantIds: removedPeerIds, block: removeBlock) - |> map { result -> Bool in - switch result { - case .success: - return true - case .pollBlocksAndRetry: - return false + let currentIds = state.getParticipantIds() + let remainingIds = pendingKickPeers.filter({ currentIds.contains($0.id._internalGetInt64Value()) }) + if remainingIds.isEmpty { + return nil + } + + return state.generateRemoveParticipantsBlock(participantIds: remainingIds.map { $0.id._internalGetInt64Value() }) + }) + if let removeBlock { + self.synchronizeRemovedParticipantsDisposable = (engine.calls.removeGroupCallBlockchainParticipants(callId: callId, accessHash: accessHash, mode: .kick, participantIds: pendingKickPeers.map { $0.id._internalGetInt64Value() }, block: removeBlock) + |> map { result -> Bool in + switch result { + case .success: + return true + case .pollBlocksAndRetry: + return false + } + } + |> deliverOnMainQueue).startStrict(next: { [weak self] shouldRetry in + guard let self else { + return + } + + if shouldRetry { + for id in pendingKickPeers { + if !self.pendingKickPeers.contains(id) { + self.pendingKickPeers.append(id) + } + } + } + + self.isSynchronizingRemovedParticipants = false + if self.scheduledSynchronizeRemovedParticipants { + self.scheduledSynchronizeRemovedParticipants = false + self.synchronizeRemovedParticipants() + } else if shouldRetry && !self.scheduledSynchronizeRemovedParticipantsAfterPoll { + self.scheduledSynchronizeRemovedParticipantsAfterPoll = true + self.e2ePoll(subChainId: 0) + } + }) + } else { + self.isSynchronizingRemovedParticipants = false + if self.scheduledSynchronizeRemovedParticipants { + self.scheduledSynchronizeRemovedParticipants = false + self.synchronizeRemovedParticipants() } } + } else { + 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, mode: .cleanup, 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) + } + }) + } + } + + func kickPeer(id: EnginePeer.Id) { + //TODO:release + if !self.pendingKickPeers.contains(id) { + self.pendingKickPeers.append(id) + + self.synchronizeRemovedParticipants() } - |> 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) - } - }) } } @@ -349,4 +443,10 @@ public final class ConferenceCallE2EContext { impl.synchronizeRemovedParticipants() } } + + public func kickPeer(id: EnginePeer.Id) { + self.impl.with { impl in + impl.kickPeer(id: id) + } + } } diff --git a/third-party/td/TdBinding/Sources/TdBinding.mm b/third-party/td/TdBinding/Sources/TdBinding.mm index e09decbae0..09ac508a27 100644 --- a/third-party/td/TdBinding/Sources/TdBinding.mm +++ b/third-party/td/TdBinding/Sources/TdBinding.mm @@ -83,6 +83,13 @@ static NSString *hexStringFromData(NSData *data) { if (self != nil) { _callId = callId; _keyPair = keyPair; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + #if DEBUG + tde2e_api::set_log_verbosity_level(4); + #endif + }); } return self; } @@ -130,7 +137,17 @@ static NSString *hexStringFromData(NSData *data) { #if DEBUG auto describeResult = tde2e_api::call_describe_message(it); if (describeResult.is_ok()) { - NSLog(@"TdCall.takeOutgoingBroadcastBlocks call_pull_outbound_messages: block %@", [[NSString alloc] initWithBytes:describeResult.value().data() length:describeResult.value().size() encoding:NSUTF8StringEncoding]); + NSString *utf8String = [[NSString alloc] initWithBytes:describeResult.value().data() length:describeResult.value().size() encoding:NSUTF8StringEncoding]; + if (utf8String) { + NSLog(@"TdCall.call_pull_outbound_messages block: %@", utf8String); + } else { + NSString *lossyString = [[NSString alloc] initWithData:[NSData dataWithBytes:describeResult.value().data() length:describeResult.value().size()] encoding:NSASCIIStringEncoding]; + if (lossyString) { + NSLog(@"TdCall.call_pull_outbound_messages block (lossy conversion): %@", lossyString); + } else { + NSLog(@"TdCall.call_pull_outbound_messages block: [binary data, length: %lu]", (unsigned long)describeResult.value().size()); + } + } } else { NSLog(@"TdCall.takeOutgoingBroadcastBlocks call_pull_outbound_messages: describe block failed"); } @@ -178,7 +195,17 @@ static NSString *hexStringFromData(NSData *data) { #if DEBUG auto describeResult = tde2e_api::call_describe_block(mappedBlock); if (describeResult.is_ok()) { - NSLog(@"TdCall.applyBlock block: %@", [[NSString alloc] initWithBytes:describeResult.value().data() length:describeResult.value().size() encoding:NSUTF8StringEncoding]); + NSString *utf8String = [[NSString alloc] initWithBytes:describeResult.value().data() length:describeResult.value().size() encoding:NSUTF8StringEncoding]; + if (utf8String) { + NSLog(@"TdCall.applyBlock block: %@", utf8String); + } else { + NSString *lossyString = [[NSString alloc] initWithData:[NSData dataWithBytes:describeResult.value().data() length:describeResult.value().size()] encoding:NSASCIIStringEncoding]; + if (lossyString) { + NSLog(@"TdCall.applyBlock block (lossy conversion): %@", lossyString); + } else { + NSLog(@"TdCall.applyBlock block: [binary data, length: %lu]", (unsigned long)describeResult.value().size()); + } + } } else { NSLog(@"TdCall.applyBlock block: describe block failed"); } @@ -196,7 +223,17 @@ static NSString *hexStringFromData(NSData *data) { #if DEBUG auto describeResult = tde2e_api::call_describe_message(mappedBlock); if (describeResult.is_ok()) { - NSLog(@"TdCall.applyBroadcastBlock block: %@", [[NSString alloc] initWithBytes:describeResult.value().data() length:describeResult.value().size() encoding:NSUTF8StringEncoding]); + NSString *utf8String = [[NSString alloc] initWithBytes:describeResult.value().data() length:describeResult.value().size() encoding:NSUTF8StringEncoding]; + if (utf8String) { + NSLog(@"TdCall.applyBroadcastBlock block: %@", utf8String); + } else { + NSString *lossyString = [[NSString alloc] initWithData:[NSData dataWithBytes:describeResult.value().data() length:describeResult.value().size()] encoding:NSASCIIStringEncoding]; + if (lossyString) { + NSLog(@"TdCall.applyBroadcastBlock block (lossy conversion): %@", lossyString); + } else { + NSLog(@"TdCall.applyBroadcastBlock block: [binary data, length: %lu]", (unsigned long)describeResult.value().size()); + } + } } else { NSLog(@"TdCall.applyBroadcastBlock block: describe block failed"); } @@ -206,6 +243,23 @@ static NSString *hexStringFromData(NSData *data) { if (!result.is_ok()) { return; } + + describeResult = tde2e_api::call_describe(_callId); + if (describeResult.is_ok()) { + NSString *utf8String = [[NSString alloc] initWithBytes:describeResult.value().data() length:describeResult.value().size() encoding:NSUTF8StringEncoding]; + if (utf8String) { + NSLog(@"TdCall.applyBroadcastBlock call after apply: %@", utf8String); + } else { + NSString *lossyString = [[NSString alloc] initWithData:[NSData dataWithBytes:describeResult.value().data() length:describeResult.value().size()] encoding:NSASCIIStringEncoding]; + if (lossyString) { + NSLog(@"TdCall.applyBroadcastBlock call after apply (lossy conversion): %@", lossyString); + } else { + NSLog(@"TdCall.applyBroadcastBlock call after apply: [binary data, length: %lu]", (unsigned long)describeResult.value().size()); + } + } + } else { + NSLog(@"TdCall.applyBroadcastBlock call after apply: describe block failed"); + } } - (nullable NSData *)generateRemoveParticipantsBlock:(NSArray *)participantIds { diff --git a/third-party/td/build-td-bazel.sh b/third-party/td/build-td-bazel.sh index e4f0c6201c..df4d257c4a 100755 --- a/third-party/td/build-td-bazel.sh +++ b/third-party/td/build-td-bazel.sh @@ -14,7 +14,6 @@ options="" options="$options -DOPENSSL_FOUND=1" options="$options -DOPENSSL_CRYPTO_LIBRARY=${openssl_crypto_library}" options="$options -DOPENSSL_INCLUDE_DIR=${OPENSSL_DIR}/src/include" -#options="$options -DOPENSSL_LIBRARIES=${openssl_crypto_library}" options="$options -DCMAKE_BUILD_TYPE=Release" cd "$BUILD_DIR"