diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 23ac7ddcb1..dbd038b4a0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14188,3 +14188,8 @@ Sorry for the inconvenience."; "SendInviteLink.TextCallsRestrictedMultipleUsers_1" = "{user_list}, and **%d** more person do not accept calls."; "SendInviteLink.TextCallsRestrictedMultipleUsers_any" = "{user_list}, and **%d** more people do not accept calls."; "SendInviteLink.TextCallsRestrictedSendInviteLink" = "You can try to send an invite link instead."; + +"Call.VoiceChatInProgressConferenceMessage" = "Leave voice chat in %1$@ and start a conference?"; +"Call.LiveStreamInProgressConferenceMessage" = "Leave live stream in %1$@ and start a conference?"; + +"VoiceChat.RemoveConferencePeerConfirmation" = "Are you sure you want to remove %@ from this call?"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index b542956c50..84aadf54c9 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -964,14 +964,16 @@ public enum JoinSubjectScreenMode { public let inviter: EnginePeer? public let members: [EnginePeer] public let totalMemberCount: Int + public let info: JoinCallLinkInformation - public init(id: Int64, accessHash: Int64, slug: String, inviter: EnginePeer?, members: [EnginePeer], totalMemberCount: Int) { + public init(id: Int64, accessHash: Int64, slug: String, inviter: EnginePeer?, members: [EnginePeer], totalMemberCount: Int, info: JoinCallLinkInformation) { self.id = id self.accessHash = accessHash self.slug = slug self.inviter = inviter self.members = members self.totalMemberCount = totalMemberCount + self.info = info } } @@ -1325,6 +1327,7 @@ public protocol AccountContext: AnyObject { func scheduleGroupCall(peerId: PeerId, parentController: ViewController) func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: EngineGroupCallDescription) + func joinConferenceCall(call: JoinCallLinkInformation, isVideo: Bool) func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void) } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index f69b179933..040aa2bab3 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -579,6 +579,7 @@ public protocol PresentationCallManager: AnyObject { initialCall: EngineGroupCallDescription, reference: InternalGroupCallReference, beginWithVideo: Bool, - invitePeerIds: [EnginePeer.Id] - ) + invitePeerIds: [EnginePeer.Id], + endCurrentIfAny: Bool + ) -> JoinGroupCallManagerResult } diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 80f86ad4a2..9475b141c7 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -210,7 +210,7 @@ public final class CallListController: TelegramBaseController { } private func createGroupCall(peerIds: [EnginePeer.Id], isVideo: Bool, completion: (() -> Void)? = nil) { - self.view.endEditing(true) + self.view.window?.endEditing(true) guard !self.presentAccountFrozenInfoIfNeeded() else { return @@ -264,7 +264,7 @@ public final class CallListController: TelegramBaseController { guard let self else { return } - self.context.sharedContext.callManager?.joinConferenceCall( + let _ = self.context.sharedContext.callManager?.joinConferenceCall( accountContext: self.context, initialCall: EngineGroupCallDescription( id: call.callInfo.id, @@ -276,7 +276,8 @@ public final class CallListController: TelegramBaseController { ), reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), beginWithVideo: isVideo, - invitePeerIds: peerIds + invitePeerIds: peerIds, + endCurrentIfAny: true ) completion?() } @@ -712,20 +713,7 @@ public final class CallListController: TelegramBaseController { guard let self else { return } - self.context.sharedContext.callManager?.joinConferenceCall( - accountContext: self.context, - initialCall: EngineGroupCallDescription( - id: resolvedCallLink.id, - accessHash: resolvedCallLink.accessHash, - title: nil, - scheduleTimestamp: nil, - subscribedToScheduled: false, - isStream: false - ), - reference: .message(id: message.id), - beginWithVideo: conferenceCall.flags.contains(.isVideo), - invitePeerIds: [] - ) + self.context.joinConferenceCall(call: resolvedCallLink, isVideo: conferenceCall.flags.contains(.isVideo)) }, error: { [weak self] error in guard let self else { return diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 4e29b6eb61..ee236408c6 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -799,7 +799,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis return entries } -private func preparedContactListNodeTransition(context: AccountContext, presentationData: PresentationData, from fromEntries: [ContactListNodeEntry], to toEntries: [ContactListNodeEntry], interaction: ContactListNodeInteraction, firstTime: Bool, isEmpty: Bool, generateIndexSections: Bool, animation: ContactListAnimation, isSearch: Bool) -> ContactsListNodeTransition { +private func preparedContactListNodeTransition(context: AccountContext, presentationData: PresentationData, from fromEntries: [ContactListNodeEntry], to toEntries: [ContactListNodeEntry], interaction: ContactListNodeInteraction, firstTime: Bool, isEmpty: Bool, hasOptions: Bool, generateIndexSections: Bool, animation: ContactListAnimation, isSearch: Bool) -> ContactsListNodeTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } @@ -848,7 +848,7 @@ private func preparedContactListNodeTransition(context: AccountContext, presenta scrollToItem = ListViewScrollToItem(index: 0, position: .top(-50.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) } - return ContactsListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, indexSections: indexSections, firstTime: firstTime, isEmpty: isEmpty, scrollToItem: scrollToItem, animation: animation) + return ContactsListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, indexSections: indexSections, firstTime: firstTime, isEmpty: isEmpty, hasOptions: hasOptions, scrollToItem: scrollToItem, animation: animation) } private struct ContactsListNodeTransition { @@ -858,6 +858,7 @@ private struct ContactsListNodeTransition { let indexSections: [String] let firstTime: Bool let isEmpty: Bool + let hasOptions: Bool let scrollToItem: ListViewScrollToItem? let animation: ContactListAnimation } @@ -1184,7 +1185,7 @@ public final class ContactListNode: ASDisplayNode { authorizeImpl?() }, openPrivacyPolicy: { openPrivacyPolicyImpl?() - }) + }, filterHitTest: true) self.authorizationNode.isHidden = true super.init() @@ -1644,7 +1645,7 @@ public final class ContactListNode: ASDisplayNode { let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: peerRequiresPremiumForMessaging, peersWithStories: [:], authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil, topPeers: [], topPeersPresentation: .none, isPeerEnabled: isPeerEnabled, interaction: interaction) let previous = previousEntries.swap(entries) - return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch)) + return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, hasOptions: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch)) } if OSAtomicCompareAndSwap32(1, 0, &firstTime) { @@ -1885,7 +1886,7 @@ public final class ContactListNode: ASDisplayNode { animation = .none } - return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: isEmpty, generateIndexSections: generateSections, animation: animation, isSearch: isSearch)) + return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: isEmpty, hasOptions: optionsCount != 0, generateIndexSections: generateSections, animation: animation, isSearch: isSearch)) } if OSAtomicCompareAndSwap32(1, 0, &firstTime) { @@ -1921,7 +1922,7 @@ public final class ContactListNode: ASDisplayNode { authorizeImpl?() }, openPrivacyPolicy: { openPrivacyPolicyImpl?() - }) + }, filterHitTest: true) strongSelf.authorizationNode.isHidden = authorizationPreviousHidden strongSelf.addSubnode(strongSelf.authorizationNode) @@ -2125,7 +2126,7 @@ public final class ContactListNode: ASDisplayNode { } }) - self.listNode.isHidden = self.displayPermissionPlaceholder && transition.isEmpty + self.listNode.isHidden = self.displayPermissionPlaceholder && (transition.isEmpty && !transition.hasOptions) self.authorizationNode.isHidden = !transition.isEmpty || !self.displayPermissionPlaceholder } } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index 34a52902e2..081116659f 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -589,6 +589,8 @@ public final class InviteLinkInviteController: ViewController { } return false }), in: .window(.root)) + + strongSelf.controller?.dismiss() } }) } diff --git a/submodules/Postbox/Sources/SqliteValueBox.swift b/submodules/Postbox/Sources/SqliteValueBox.swift index 745c094015..a89f8f21d5 100644 --- a/submodules/Postbox/Sources/SqliteValueBox.swift +++ b/submodules/Postbox/Sources/SqliteValueBox.swift @@ -149,7 +149,7 @@ struct SqlitePreparedStatement { } } -private let dabaseFileNames: [String] = [ +private let databaseFileNames: [String] = [ "db_sqlite", "db_sqlite-shm", "db_sqlite-wal" @@ -194,7 +194,6 @@ public final class SqliteValueBox: ValueBox { private var insertOrIgnorePrimaryKeyStatements: [Int32 : SqlitePreparedStatement] = [:] private var insertOrIgnoreIndexKeyStatements: [Int32 : SqlitePreparedStatement] = [:] private var deleteStatements: [Int32 : SqlitePreparedStatement] = [:] - private var moveStatements: [Int32 : SqlitePreparedStatement] = [:] private var copyStatements: [TablePairKey : SqlitePreparedStatement] = [:] private var fullTextInsertStatements: [Int32 : SqlitePreparedStatement] = [:] private var fullTextDeleteStatements: [Int32 : SqlitePreparedStatement] = [:] @@ -347,7 +346,7 @@ public final class SqliteValueBox: ValueBox { return nil } - for fileName in dabaseFileNames { + for fileName in databaseFileNames { let _ = try? FileManager.default.removeItem(atPath: basePath + "/\(fileName)") } database = Database(path, readOnly: false)! @@ -367,7 +366,7 @@ public final class SqliteValueBox: ValueBox { } assert(false) - for fileName in dabaseFileNames { + for fileName in databaseFileNames { let _ = try? FileManager.default.removeItem(atPath: basePath + "/\(fileName)") } @@ -400,7 +399,7 @@ public final class SqliteValueBox: ValueBox { if self.isEncrypted(database) { postboxLog("Reencryption failed") - for fileName in dabaseFileNames { + for fileName in databaseFileNames { let _ = try? FileManager.default.removeItem(atPath: basePath + "/\(fileName)") } database = Database(path, readOnly: false)! @@ -427,7 +426,7 @@ public final class SqliteValueBox: ValueBox { return nil } - for fileName in dabaseFileNames { + for fileName in databaseFileNames { let _ = try? FileManager.default.removeItem(atPath: basePath + "/\(fileName)") } database = Database(path, readOnly: false)! @@ -1224,7 +1223,7 @@ public final class SqliteValueBox: ValueBox { preconditionFailure(errorText) } let preparedStatement = SqlitePreparedStatement(statement: statement) - self.insertOrReplacePrimaryKeyStatements[table.table.id] = preparedStatement + self.insertOrReplaceIndexKeyStatements[table.table.id] = preparedStatement resultStatement = preparedStatement } } @@ -1279,7 +1278,7 @@ public final class SqliteValueBox: ValueBox { preconditionFailure(errorText) } let preparedStatement = SqlitePreparedStatement(statement: statement) - self.insertOrIgnorePrimaryKeyStatements[table.table.id] = preparedStatement + self.insertOrIgnoreIndexKeyStatements[table.table.id] = preparedStatement resultStatement = preparedStatement } } @@ -1330,38 +1329,6 @@ public final class SqliteValueBox: ValueBox { return resultStatement } - private func moveStatement(_ table: ValueBoxTable, from previousKey: ValueBoxKey, to updatedKey: ValueBoxKey) -> SqlitePreparedStatement { - precondition(self.queue.isCurrent()) - checkTableKey(table, previousKey) - checkTableKey(table, updatedKey) - - let resultStatement: SqlitePreparedStatement - - if let statement = self.moveStatements[table.id] { - resultStatement = statement - } else { - var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v3(self.database.handle, "UPDATE t\(table.id) SET key=? WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) - precondition(status == SQLITE_OK) - let preparedStatement = SqlitePreparedStatement(statement: statement) - self.moveStatements[table.id] = preparedStatement - resultStatement = preparedStatement - } - - resultStatement.reset() - - switch table.keyType { - case .binary: - resultStatement.bind(1, data: previousKey.memory, length: previousKey.length) - resultStatement.bind(2, data: updatedKey.memory, length: updatedKey.length) - case .int64: - resultStatement.bind(1, number: previousKey.getInt64(0)) - resultStatement.bind(2, number: updatedKey.getInt64(0)) - } - - return resultStatement - } - private func copyStatement(fromTable: ValueBoxTable, fromKey: ValueBoxKey, toTable: ValueBoxTable, toKey: ValueBoxKey) -> SqlitePreparedStatement { precondition(self.queue.isCurrent()) let _ = checkTable(fromTable) @@ -1443,6 +1410,8 @@ public final class SqliteValueBox: ValueBox { } private func fullTextDeleteStatement(_ table: ValueBoxFullTextTable, itemId: Data) -> SqlitePreparedStatement { + precondition(self.queue.isCurrent()) + let resultStatement: SqlitePreparedStatement if let statement = self.fullTextDeleteStatements[table.id] { @@ -2004,16 +1973,6 @@ public final class SqliteValueBox: ValueBox { } } - public func move(_ table: ValueBoxTable, from previousKey: ValueBoxKey, to updatedKey: ValueBoxKey) { - precondition(self.queue.isCurrent()) - if let _ = self.tables[table.id] { - let statement = self.moveStatement(table, from: previousKey, to: updatedKey) - while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { - } - statement.reset() - } - } - public func copy(fromTable: ValueBoxTable, fromKey: ValueBoxKey, toTable: ValueBoxTable, toKey: ValueBoxKey) { precondition(self.queue.isCurrent()) if let _ = self.tables[fromTable.id] { @@ -2254,11 +2213,6 @@ public final class SqliteValueBox: ValueBox { } self.deleteStatements.removeAll() - for (_, statement) in self.moveStatements { - statement.destroy() - } - self.moveStatements.removeAll() - for (_, statement) in self.copyStatements { statement.destroy() } @@ -2319,7 +2273,7 @@ public final class SqliteValueBox: ValueBox { postboxLog("dropping DB") - for fileName in dabaseFileNames { + for fileName in databaseFileNames { let _ = try? FileManager.default.removeItem(atPath: self.basePath + "/\(fileName)") } @@ -2368,7 +2322,7 @@ public final class SqliteValueBox: ValueBox { self.exportEncrypted(database: database, to: targetPath, encryptionParameters: encryptionParameters) - for name in dabaseFileNames { + for name in databaseFileNames { let _ = try? FileManager.default.removeItem(atPath: self.basePath + "/\(name)") let _ = try? FileManager.default.moveItem(atPath: targetPath + "/\(name)", toPath: self.basePath + "/\(name)") } diff --git a/submodules/Postbox/Sources/ValueBox.swift b/submodules/Postbox/Sources/ValueBox.swift index bdc4010f1f..f4f5bdafa6 100644 --- a/submodules/Postbox/Sources/ValueBox.swift +++ b/submodules/Postbox/Sources/ValueBox.swift @@ -85,7 +85,6 @@ public protocol ValueBox { func exists(_ table: ValueBoxTable, key: ValueBoxKey) -> Bool func set(_ table: ValueBoxTable, key: ValueBoxKey, value: MemoryBuffer) func remove(_ table: ValueBoxTable, key: ValueBoxKey, secure: Bool) - func move(_ table: ValueBoxTable, from previousKey: ValueBoxKey, to updatedKey: ValueBoxKey) func copy(fromTable: ValueBoxTable, fromKey: ValueBoxKey, toTable: ValueBoxTable, toKey: ValueBoxKey) func removeRange(_ table: ValueBoxTable, start: ValueBoxKey, end: ValueBoxKey) func fullTextSet(_ table: ValueBoxFullTextTable, collectionId: String, itemId: String, contents: String, tags: String) diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 07010fc4a8..7872e0a3cc 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -1065,4 +1065,42 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { }) }, activeCall: activeCall) } + + open func joinConferenceCall(message: EngineMessage) { + var action: TelegramMediaAction? + for media in message.media { + if let media = media as? TelegramMediaAction { + action = media + break + } + } + guard case let .conferenceCall(conferenceCall) = action?.action else { + return + } + + if let currentGroupCallController = self.context.sharedContext.currentGroupCallController as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == conferenceCall.callId { + self.context.sharedContext.navigateToCurrentCall() + return + } + + let signal = self.context.engine.peers.joinCallInvitationInformation(messageId: message.id) + let _ = (signal + |> deliverOnMainQueue).startStandalone(next: { [weak self] resolvedCallLink in + guard let self else { + return + } + self.context.joinConferenceCall(call: resolvedCallLink, isVideo: conferenceCall.flags.contains(.isVideo)) + }, error: { [weak self] error in + guard let self else { + return + } + switch error { + case .doesNotExist: + self.context.sharedContext.openCreateGroupCallUI(context: self.context, peerIds: conferenceCall.otherParticipants, parentController: self) + default: + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.present(textAlertController(context: self.context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + }) + } } diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index 57b06878a5..ceec744d40 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -1084,35 +1084,83 @@ public final class PresentationCallManagerImpl: PresentationCallManager { initialCall: EngineGroupCallDescription, reference: InternalGroupCallReference, beginWithVideo: Bool, - invitePeerIds: [EnginePeer.Id] - ) { - let keyPair: TelegramKeyPair - guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else { - return + invitePeerIds: [EnginePeer.Id], + endCurrentIfAny: Bool + ) -> JoinGroupCallManagerResult { + let begin: () -> Void = { [weak self] in + guard let self else { + return + } + + let keyPair: TelegramKeyPair + guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else { + return + } + keyPair = keyPairValue + + let call = PresentationGroupCallImpl( + accountContext: accountContext, + audioSession: self.audioSession, + callKitIntegration: nil, + getDeviceAccessData: self.getDeviceAccessData, + initialCall: (initialCall, reference), + internalId: CallSessionInternalId(), + peerId: nil, + isChannel: false, + invite: nil, + joinAsPeerId: nil, + isStream: false, + keyPair: keyPair, + conferenceSourceId: nil, + isConference: true, + beginWithVideo: beginWithVideo, + sharedAudioContext: nil + ) + for peerId in invitePeerIds { + let _ = call.invitePeer(peerId, isVideo: beginWithVideo) + } + self.updateCurrentGroupCall(.group(call)) } - keyPair = keyPairValue - let call = PresentationGroupCallImpl( - accountContext: accountContext, - audioSession: self.audioSession, - callKitIntegration: nil, - getDeviceAccessData: self.getDeviceAccessData, - initialCall: (initialCall, reference), - internalId: CallSessionInternalId(), - peerId: nil, - isChannel: false, - invite: nil, - joinAsPeerId: nil, - isStream: false, - keyPair: keyPair, - conferenceSourceId: nil, - isConference: true, - beginWithVideo: beginWithVideo, - sharedAudioContext: nil - ) - for peerId in invitePeerIds { - let _ = call.invitePeer(peerId, isVideo: beginWithVideo) + if let currentGroupCall = self.currentGroupCallValue { + if endCurrentIfAny { + switch currentGroupCall { + case let .conferenceSource(conferenceSource): + self.startCallDisposable.set((conferenceSource.hangUp() + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + begin() + })) + case let .group(groupCall): + self.startCallDisposable.set((groupCall.leave(terminateIfPossible: false) + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + begin() + })) + } + } else { + switch currentGroupCall { + case let .conferenceSource(conferenceSource): + return .alreadyInProgress(conferenceSource.peerId) + case let .group(groupCall): + return .alreadyInProgress(groupCall.peerId) + } + } + } else if let currentCall = self.currentCall { + if endCurrentIfAny { + self.callKitIntegration?.dropCall(uuid: currentCall.internalId) + self.startCallDisposable.set((currentCall.hangUp() + |> deliverOnMainQueue).start(next: { _ in + begin() + })) + } else { + return .alreadyInProgress(currentCall.peerId) + } + } else { + begin() } - self.updateCurrentGroupCall(.group(call)) + return .joined } } diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 7c78617788..e68b38cabe 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -3394,6 +3394,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } private func updateProximityMonitoring() { + if self.sharedAudioContext != nil { + return + } + var shouldMonitorProximity = false switch self.currentSelectedAudioOutputValue { case .builtin: diff --git a/submodules/TelegramCallsUI/Sources/SharedCallAudioContext.swift b/submodules/TelegramCallsUI/Sources/SharedCallAudioContext.swift index 7bc847b190..1ab045575d 100644 --- a/submodules/TelegramCallsUI/Sources/SharedCallAudioContext.swift +++ b/submodules/TelegramCallsUI/Sources/SharedCallAudioContext.swift @@ -2,6 +2,7 @@ import Foundation import SwiftSignalKit import TelegramVoip import TelegramAudio +import DeviceProximity public final class SharedCallAudioContext { private static weak var current: SharedCallAudioContext? @@ -33,6 +34,8 @@ public final class SharedCallAudioContext { private let audioSessionShouldBeActive = Promise(true) private var initialSetupTimer: Foundation.Timer? + + private var proximityManagerIndex: Int? static func get(audioSession: ManagedAudioSession, callKitIntegration: CallKitIntegration?, defaultToSpeaker: Bool = false, reuseCurrent: Bool = false) -> SharedCallAudioContext { if let current = self.current, reuseCurrent { @@ -105,6 +108,8 @@ public final class SharedCallAudioContext { audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue)) audioSessionControl.setup(synchronous: true) } + + self.updateProximityMonitoring() } }) } @@ -129,6 +134,7 @@ public final class SharedCallAudioContext { if let currentOutput = currentOutput { self.currentAudioOutputValue = currentOutput self.didSetCurrentAudioOutputValue = true + self.updateProximityMonitoring() } var signal: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> = .single((availableOutputs, currentOutput)) @@ -186,6 +192,7 @@ public final class SharedCallAudioContext { self.audioOutputStateValue = value if let currentOutput = value.1 { self.currentAudioOutputValue = currentOutput + self.updateProximityMonitoring() } }) } @@ -196,6 +203,10 @@ public final class SharedCallAudioContext { self.isAudioSessionActiveDisposable?.dispose() self.audioOutputStateDisposable?.dispose() self.initialSetupTimer?.invalidate() + + if let proximityManagerIndex = self.proximityManagerIndex { + DeviceProximityManager.shared().remove(proximityManagerIndex) + } } func setCurrentAudioOutput(_ output: AudioSessionOutput) { @@ -228,4 +239,26 @@ public final class SharedCallAudioContext { self.setCurrentAudioOutput(.speaker) } } + + private func updateProximityMonitoring() { + var shouldMonitorProximity = false + switch self.currentAudioOutputValue { + case .builtin: + shouldMonitorProximity = true + default: + break + } + + if shouldMonitorProximity { + if self.proximityManagerIndex == nil { + self.proximityManagerIndex = DeviceProximityManager.shared().add { _ in + } + } + } else { + if let proximityManagerIndex = self.proximityManagerIndex { + self.proximityManagerIndex = nil + DeviceProximityManager.shared().remove(proximityManagerIndex) + } + } + } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift index 6156b426d7..86399fe9ae 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift @@ -566,7 +566,7 @@ final class VideoChatMicButtonComponent: Component { transition.setPosition(view: self.icon.view, position: iconFrame.center) transition.setBounds(view: self.icon.view, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) - transition.setScale(view: self.icon.view, scale: component.isCollapsed ? ((iconSize.width - 24.0) / iconSize.width) : 1.0) + transition.setScale(view: self.icon.view, scale: component.isCollapsed ? ((iconSize.width - 32.0) / iconSize.width) : 1.0) switch component.content { case .connecting: diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 5fd4a9d86c..54bc0d4a64 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -25,12 +25,16 @@ final class VideoChatParticipantsComponent: Component { } } + var leftInset: CGFloat + var rightInset: CGFloat var videoColumn: Column? var mainColumn: Column var columnSpacing: CGFloat var isMainColumnHidden: Bool - init(videoColumn: Column?, mainColumn: Column, columnSpacing: CGFloat, isMainColumnHidden: Bool) { + init(leftInset: CGFloat, rightInset: CGFloat, videoColumn: Column?, mainColumn: Column, columnSpacing: CGFloat, isMainColumnHidden: Bool) { + self.leftInset = leftInset + self.rightInset = rightInset self.videoColumn = videoColumn self.mainColumn = mainColumn self.columnSpacing = columnSpacing @@ -509,13 +513,13 @@ final class VideoChatParticipantsComponent: Component { if let videoColumn = layout.videoColumn { let columnsWidth: CGFloat = videoColumn.width + layout.columnSpacing + layout.mainColumn.width - let columnsSideInset: CGFloat = floorToScreenPixels((containerSize.width - columnsWidth) * 0.5) + let columnsLeftInset: CGFloat = layout.leftInset - var separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: gridWidth, height: containerSize.height)) + var separateVideoGridFrame = CGRect(origin: CGPoint(x: columnsLeftInset, y: 0.0), size: CGSize(width: gridWidth, height: containerSize.height)) var listFrame = CGRect(origin: CGPoint(x: separateVideoGridFrame.maxX + layout.columnSpacing, y: 0.0), size: CGSize(width: listWidth, height: containerSize.height)) if isUIHidden || layout.isMainColumnHidden { - listFrame.origin.x = containerSize.width + columnsSideInset + listFrame.origin.x = containerSize.width + columnsLeftInset separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: columnsWidth, height: containerSize.height)) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 533e13905d..909485deb0 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -387,7 +387,6 @@ final class VideoChatScreenComponent: Component { let targetContainer = SimpleLayer() targetContainer.masksToBounds = true - targetContainer.backgroundColor = UIColor.blue.cgColor targetContainer.cornerRadius = 10.0 self.containerView.layer.insertSublayer(targetContainer, above: participantsView.layer) @@ -1276,11 +1275,61 @@ final class VideoChatScreenComponent: Component { ) } + var firstMemberWithPresentation: VideoChatParticipantsComponent.VideoParticipantKey? + if let members { + for participant in members.participants { + if participant.presentationDescription != nil { + firstMemberWithPresentation = VideoChatParticipantsComponent.VideoParticipantKey(id: participant.id, isPresentation: true) + break + } + } + } + if let previousMembers = self.members, let firstMemberWithPresentationValue = firstMemberWithPresentation { + for participant in previousMembers.participants { + if participant.id == firstMemberWithPresentationValue.id && participant.presentationDescription != nil { + firstMemberWithPresentation = nil + break + } + } + } else { + firstMemberWithPresentation = nil + } + + if let expandedParticipantsVideoState = self.expandedParticipantsVideoState { + if expandedParticipantsVideoState.isMainParticipantPinned { + firstMemberWithPresentation = nil + } + } else { + firstMemberWithPresentation = nil + } + + if let members, firstMemberWithPresentation != nil { + var videoCount = 0 + for participant in members.participants { + if participant.presentationDescription != nil { + videoCount += 1 + } + if participant.videoDescription != nil { + videoCount += 1 + } + } + if videoCount <= 1 { + firstMemberWithPresentation = nil + } + } else { + firstMemberWithPresentation = nil + } + self.members = members + if let members { self.invitedPeers.removeAll(where: { invitedPeer in members.participants.contains(where: { $0.id == .peer(invitedPeer.peer.id) }) }) } + if let firstMemberWithPresentation { + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: firstMemberWithPresentation, isMainParticipantPinned: true, isUIHidden: self.expandedParticipantsVideoState?.isUIHidden ?? false) + } + if let members, let expandedParticipantsVideoState = self.expandedParticipantsVideoState, !expandedParticipantsVideoState.isUIHidden { var videoCount = 0 for participant in members.participants { @@ -1886,7 +1935,18 @@ final class VideoChatScreenComponent: Component { } }) - let sideInset: CGFloat = max(environment.safeInsets.left, 14.0) + let landscapeControlsWidth: CGFloat = 88.0 + let landscapeControlsOffsetX: CGFloat = 18.0 + let landscapeControlsSpacing: CGFloat = 30.0 + + let leftInset: CGFloat = max(environment.safeInsets.left, 14.0) + + var rightInset: CGFloat = max(environment.safeInsets.right, 14.0) + var buttonsOnTheSide = false + if availableSize.width > maxSingleColumnWidth && !environment.metrics.isTablet { + buttonsOnTheSide = true + rightInset += landscapeControlsWidth + } let topInset: CGFloat = environment.statusBarHeight + 2.0 let navigationBarHeight: CGFloat = 61.0 @@ -1944,7 +2004,7 @@ final class VideoChatScreenComponent: Component { containerSize: CGSize(width: navigationButtonDiameter, height: navigationButtonDiameter) ) - let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: sideInset + floor((navigationButtonAreaWidth - navigationLeftButtonSize.width) * 0.5), y: topInset + floor((navigationBarHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize) + let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: leftInset + floor((navigationButtonAreaWidth - navigationLeftButtonSize.width) * 0.5), y: topInset + floor((navigationBarHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize) if let navigationLeftButtonView = self.navigationLeftButton.view { if navigationLeftButtonView.superview == nil { self.containerView.addSubview(navigationLeftButtonView) @@ -1953,7 +2013,7 @@ final class VideoChatScreenComponent: Component { alphaTransition.setAlpha(view: navigationLeftButtonView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0) } - let navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - navigationButtonAreaWidth + floor((navigationButtonAreaWidth - navigationRightButtonSize.width) * 0.5), y: topInset + floor((navigationBarHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize) + let navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - navigationButtonAreaWidth + floor((navigationButtonAreaWidth - navigationRightButtonSize.width) * 0.5), y: topInset + floor((navigationBarHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize) if let navigationRightButtonView = self.navigationRightButton.view { if navigationRightButtonView.superview == nil { self.containerView.addSubview(navigationRightButtonView) @@ -2037,7 +2097,7 @@ final class VideoChatScreenComponent: Component { let canManageCall = self.callState?.canManageCall ?? false - var maxTitleWidth: CGFloat = availableSize.width - sideInset * 2.0 - navigationButtonAreaWidth * 2.0 - 4.0 * 2.0 + var maxTitleWidth: CGFloat = availableSize.width - leftInset - rightInset - navigationButtonAreaWidth * 2.0 - 4.0 * 2.0 if isTwoColumnLayout { maxTitleWidth -= 110.0 } @@ -2086,7 +2146,7 @@ final class VideoChatScreenComponent: Component { environment: {}, containerSize: CGSize(width: maxTitleWidth, height: 100.0) ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: topInset + floor((navigationBarHeight - titleSize.height) * 0.5)), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: leftInset + floor((availableSize.width - leftInset - rightInset - titleSize.width) * 0.5), y: topInset + floor((navigationBarHeight - titleSize.height) * 0.5)), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { self.containerView.addSubview(titleView) @@ -2132,9 +2192,9 @@ final class VideoChatScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: min(400.0, availableSize.width - sideInset * 2.0 - 16.0 * 2.0), height: 10000.0) + containerSize: CGSize(width: min(400.0, availableSize.width - leftInset - rightInset - 16.0 * 2.0), height: 10000.0) ) - let encryptionKeyFrameValue = CGRect(origin: CGPoint(x: floor((availableSize.width - encryptionKeySize.width) * 0.5), y: navigationHeight), size: encryptionKeySize) + let encryptionKeyFrameValue = CGRect(origin: CGPoint(x: leftInset + floor((availableSize.width - leftInset - rightInset - encryptionKeySize.width) * 0.5), y: navigationHeight), size: encryptionKeySize) encryptionKeyFrame = encryptionKeyFrameValue navigationHeight += encryptionKeySize.height @@ -2164,7 +2224,7 @@ final class VideoChatScreenComponent: Component { mainColumnSideInset = 0.0 } else { mainColumnWidth = availableSize.width - mainColumnSideInset = sideInset + mainColumnSideInset = max(leftInset, rightInset) } } @@ -2181,7 +2241,9 @@ final class VideoChatScreenComponent: Component { } let microphoneButtonDiameter: CGFloat - if isTwoColumnLayout { + if buttonsOnTheSide { + microphoneButtonDiameter = actionButtonDiameter + } else if isTwoColumnLayout { microphoneButtonDiameter = collapsedMicrophoneButtonDiameter } else { if areButtonsCollapsed { @@ -2221,11 +2283,21 @@ final class VideoChatScreenComponent: Component { } } - if isTwoColumnLayout { + if buttonsOnTheSide { + collapsedMicrophoneButtonFrame.origin.y = floor((availableSize.height - actionButtonDiameter) * 0.5) + collapsedMicrophoneButtonFrame.origin.x = availableSize.width - environment.safeInsets.right - landscapeControlsWidth + landscapeControlsOffsetX + if isMainColumnHidden { - collapsedMicrophoneButtonFrame.origin.x = availableSize.width - sideInset - mainColumnWidth + floor((mainColumnWidth - collapsedMicrophoneButtonDiameter) * 0.5) + sideInset + mainColumnWidth + collapsedMicrophoneButtonFrame.origin.x += mainColumnWidth + landscapeControlsWidth + } + + collapsedMicrophoneButtonFrame.size = CGSize(width: actionButtonDiameter, height: actionButtonDiameter) + expandedMicrophoneButtonFrame = collapsedMicrophoneButtonFrame + } else if isTwoColumnLayout { + if isMainColumnHidden { + collapsedMicrophoneButtonFrame.origin.x = availableSize.width - rightInset - mainColumnWidth + floor((mainColumnWidth - collapsedMicrophoneButtonDiameter) * 0.5) + leftInset + mainColumnWidth } else { - collapsedMicrophoneButtonFrame.origin.x = availableSize.width - sideInset - mainColumnWidth + floor((mainColumnWidth - collapsedMicrophoneButtonDiameter) * 0.5) + collapsedMicrophoneButtonFrame.origin.x = availableSize.width - rightInset - mainColumnWidth + floor((mainColumnWidth - collapsedMicrophoneButtonDiameter) * 0.5) } expandedMicrophoneButtonFrame = collapsedMicrophoneButtonFrame } else { @@ -2242,7 +2314,11 @@ final class VideoChatScreenComponent: Component { } let collapsedParticipantsClippingY: CGFloat - collapsedParticipantsClippingY = collapsedMicrophoneButtonFrame.minY - 16.0 + if buttonsOnTheSide { + collapsedParticipantsClippingY = availableSize.height + } else { + collapsedParticipantsClippingY = collapsedMicrophoneButtonFrame.minY - 16.0 + } let expandedParticipantsClippingY: CGFloat if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.isUIHidden { @@ -2255,8 +2331,16 @@ final class VideoChatScreenComponent: Component { expandedParticipantsClippingY = expandedMicrophoneButtonFrame.minY - 24.0 } - let leftActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) - let rightActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + var leftActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + var rightActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + + if buttonsOnTheSide { + leftActionButtonFrame.origin.x = microphoneButtonFrame.minX + leftActionButtonFrame.origin.y = microphoneButtonFrame.minY - landscapeControlsSpacing - actionButtonDiameter + + rightActionButtonFrame.origin.x = microphoneButtonFrame.minX + rightActionButtonFrame.origin.y = microphoneButtonFrame.maxY + landscapeControlsSpacing + } let participantsSize = availableSize @@ -2264,8 +2348,10 @@ final class VideoChatScreenComponent: Component { let participantsLayout: VideoChatParticipantsComponent.Layout if isTwoColumnLayout { let mainColumnInsets: UIEdgeInsets = UIEdgeInsets(top: navigationHeight, left: mainColumnSideInset, bottom: availableSize.height - collapsedParticipantsClippingY, right: mainColumnSideInset) - let videoColumnWidth: CGFloat = max(10.0, availableSize.width - sideInset * 2.0 - mainColumnWidth - columnSpacing) + let videoColumnWidth: CGFloat = max(10.0, availableSize.width - leftInset - rightInset - mainColumnWidth - columnSpacing) participantsLayout = VideoChatParticipantsComponent.Layout( + leftInset: leftInset, + rightInset: rightInset, videoColumn: VideoChatParticipantsComponent.Layout.Column( width: videoColumnWidth, insets: UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: max(14.0, environment.safeInsets.bottom), right: 0.0) @@ -2280,6 +2366,8 @@ final class VideoChatScreenComponent: Component { } else { let mainColumnInsets: UIEdgeInsets = UIEdgeInsets(top: navigationHeight, left: mainColumnSideInset, bottom: availableSize.height - collapsedParticipantsClippingY, right: mainColumnSideInset) participantsLayout = VideoChatParticipantsComponent.Layout( + leftInset: leftInset, + rightInset: rightInset, videoColumn: nil, mainColumn: VideoChatParticipantsComponent.Layout.Column( width: mainColumnWidth, @@ -2354,7 +2442,7 @@ final class VideoChatScreenComponent: Component { isUIHidden = alsoSetIsUIHidden } - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false, isUIHidden: isUIHidden) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: self.expandedParticipantsVideoState == nil && key.isPresentation, isUIHidden: isUIHidden) self.focusedSpeakerAutoSwitchDeadline = CFAbsoluteTimeGetCurrent() + 3.0 self.state?.updated(transition: .spring(duration: 0.4)) } else if self.expandedParticipantsVideoState != nil { @@ -2585,7 +2673,7 @@ final class VideoChatScreenComponent: Component { call: call, strings: environment.strings, content: micButtonContent, - isCollapsed: areButtonsCollapsed, + isCollapsed: areButtonsCollapsed || buttonsOnTheSide, updateUnmutedStateIsPushToTalk: { [weak self] unmutedStateIsPushToTalk in guard let self, let currentCall = self.currentCall else { return @@ -2654,7 +2742,7 @@ final class VideoChatScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: microphoneButtonDiameter, height: microphoneButtonDiameter) + containerSize: microphoneButtonFrame.size ) if let microphoneButtonView = self.microphoneButton.view { if microphoneButtonView.superview == nil { @@ -2709,14 +2797,14 @@ final class VideoChatScreenComponent: Component { } var displayVideoControlButton = true - if areButtonsCollapsed { + if areButtonsCollapsed || buttonsOnTheSide { displayVideoControlButton = false } else if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, !expandedParticipantsVideoState.isUIHidden { displayVideoControlButton = false } if case .audio = videoControlButtonContent { if let (availableOutputs, _) = self.audioOutputState { - if availableOutputs.count <= 0 { + if availableOutputs.count <= 1 { displayVideoControlButton = false } } else { @@ -2762,7 +2850,7 @@ final class VideoChatScreenComponent: Component { strings: environment.strings, content: videoButtonContent, microphoneState: actionButtonMicrophoneState, - isCollapsed: areButtonsCollapsed + isCollapsed: areButtonsCollapsed || buttonsOnTheSide )), effectAlignment: .center, action: { [weak self] in @@ -2816,7 +2904,7 @@ final class VideoChatScreenComponent: Component { strings: environment.strings, content: .leave, microphoneState: actionButtonMicrophoneState, - isCollapsed: areButtonsCollapsed + isCollapsed: areButtonsCollapsed || buttonsOnTheSide )), effectAlignment: .center, action: { [weak self] in diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift index 9542fe76c0..4ebe1863ad 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -298,6 +298,8 @@ extension VideoChatScreenComponent.View { let nameDisplayOrder = presentationData.nameDisplayOrder if let chatPeer { items.append(DeleteChatPeerActionSheetItem(context: groupCall.accountContext, peer: peer, chatPeer: chatPeer, action: .removeFromGroup, strings: environment.strings, nameDisplayOrder: nameDisplayOrder)) + } else { + items.append(ActionSheetTextItem(title: environment.strings.VoiceChat_RemoveConferencePeerConfirmation(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string, parseMarkdown: true)) } items.append(ActionSheetButtonItem(title: environment.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak self, weak actionSheet] in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift index 218cf05c84..ea763de6f6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift @@ -143,13 +143,15 @@ func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal Sign members.append(peer) } } - return .single(JoinCallLinkInformation(id: call.info.id, accessHash: call.info.accessHash, inviter: nil, members: members, totalMemberCount: call.info.participantCount)) + return .single(JoinCallLinkInformation(reference: .link(slug: hash), id: call.info.id, accessHash: call.info.accessHash, inviter: nil, members: members, totalMemberCount: call.info.participantCount)) } } @@ -204,6 +206,6 @@ func _internal_joinCallInvitationInformation(account: Account, messageId: Messag members.append(peer) } } - return .single(JoinCallLinkInformation(id: call.info.id, accessHash: call.info.accessHash, inviter: nil, members: members, totalMemberCount: call.info.participantCount)) + return .single(JoinCallLinkInformation(reference: .message(id: messageId), id: call.info.id, accessHash: call.info.accessHash, inviter: nil, members: members, totalMemberCount: call.info.participantCount)) } } diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift index 14c1cc419a..f8f301bda2 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift @@ -35,6 +35,7 @@ public enum PermissionContentIcon: Equatable { public final class PermissionContentNode: ASDisplayNode { private var theme: PresentationTheme public let kind: Int32 + private let filterHitTest: Bool private let iconNode: ASImageNode private let nearbyIconNode: PeersNearbyIconNode? @@ -55,12 +56,13 @@ public final class PermissionContentNode: ASDisplayNode { public var validLayout: (CGSize, UIEdgeInsets)? - public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, kind: Int32, icon: PermissionContentIcon, title: String, subtitle: String? = nil, text: String, buttonTitle: String, secondaryButtonTitle: String? = nil, footerText: String? = nil, buttonAction: @escaping () -> Void, openPrivacyPolicy: (() -> Void)?) { + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, kind: Int32, icon: PermissionContentIcon, title: String, subtitle: String? = nil, text: String, buttonTitle: String, secondaryButtonTitle: String? = nil, footerText: String? = nil, buttonAction: @escaping () -> Void, openPrivacyPolicy: (() -> Void)?, filterHitTest: Bool = false) { self.theme = theme self.kind = kind self.buttonAction = buttonAction self.openPrivacyPolicy = openPrivacyPolicy + self.filterHitTest = filterHitTest self.icon = icon self.title = title @@ -163,6 +165,18 @@ public final class PermissionContentNode: ASDisplayNode { self.privacyPolicyButton.addTarget(self, action: #selector(self.privacyPolicyPressed), forControlEvents: .touchUpInside) } + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + if self.filterHitTest { + if result === self.view { + return nil + } + } + return result + } + public func updatePresentationData(_ presentationData: PresentationData) { let theme = presentationData.theme self.theme = theme diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index aa5f8341be..7e109c6f1a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1392,7 +1392,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { slug: link, inviter: resolvedCallLink.inviter, members: resolvedCallLink.members, - totalMemberCount: resolvedCallLink.totalMemberCount + totalMemberCount: resolvedCallLink.totalMemberCount, + info: resolvedCallLink )))) }) case let .localization(identifier): diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift index 042be113c0..c8d934620e 100644 --- a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift @@ -394,20 +394,7 @@ private final class JoinSubjectScreenComponent: Component { self.environment?.controller()?.dismiss() }) case let .groupCall(groupCall): - component.context.sharedContext.callManager?.joinConferenceCall( - accountContext: component.context, - initialCall: EngineGroupCallDescription( - id: groupCall.id, - accessHash: groupCall.accessHash, - title: nil, - scheduleTimestamp: nil, - subscribedToScheduled: false, - isStream: false - ), - reference: .link(slug: groupCall.slug), - beginWithVideo: false, - invitePeerIds: [] - ) + component.context.joinConferenceCall(call: groupCall.info, isVideo: false) self.environment?.controller()?.dismiss() } diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index f0e9379678..0e2514e3b1 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -684,6 +684,106 @@ public final class AccountContextImpl: AccountContext { } } + public func joinConferenceCall(call: JoinCallLinkInformation, isVideo: Bool) { + guard let callManager = self.sharedContext.callManager else { + return + } + let result = callManager.joinConferenceCall( + accountContext: self, + initialCall: EngineGroupCallDescription( + id: call.id, + accessHash: call.accessHash, + title: nil, + scheduleTimestamp: nil, + subscribedToScheduled: false, + isStream: false + ), + reference: call.reference, + beginWithVideo: isVideo, + invitePeerIds: [], + endCurrentIfAny: false + ) + if case let .alreadyInProgress(currentPeerId) = result { + let dataInput: Signal + if let currentPeerId { + dataInput = self.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: currentPeerId) + ) + } else { + dataInput = .single(nil) + } + + let _ = (dataInput + |> deliverOnMainQueue).start(next: { [weak self] current in + guard let strongSelf = self else { + return + } + let presentationData = strongSelf.sharedContext.currentPresentationData.with { $0 } + if let current = current { + switch current { + case .channel, .legacyGroup: + let title: String + let text: String + if case let .channel(channel) = current, case .broadcast = channel.info { + title = presentationData.strings.Call_LiveStreamInProgressTitle + text = presentationData.strings.Call_LiveStreamInProgressConferenceMessage(current.compactDisplayTitle).string + } else { + title = presentationData.strings.Call_VoiceChatInProgressTitle + text = presentationData.strings.Call_VoiceChatInProgressConferenceMessage(current.compactDisplayTitle).string + } + + strongSelf.sharedContext.mainWindow?.present(textAlertController(context: strongSelf, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + guard let self else { + return + } + let _ = callManager.joinConferenceCall( + accountContext: self, + initialCall: EngineGroupCallDescription( + id: call.id, + accessHash: call.accessHash, + title: nil, + scheduleTimestamp: nil, + subscribedToScheduled: false, + isStream: false + ), + reference: call.reference, + beginWithVideo: isVideo, + invitePeerIds: [], + endCurrentIfAny: true + ) + })]), on: .root) + default: + let text: String + text = presentationData.strings.Call_VoiceChatInProgressConferenceMessage(current.compactDisplayTitle).string + strongSelf.sharedContext.mainWindow?.present(textAlertController(context: strongSelf, title: presentationData.strings.Call_CallInProgressTitle, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + guard let self else { + return + } + let _ = callManager.joinConferenceCall( + accountContext: self, + initialCall: EngineGroupCallDescription( + id: call.id, + accessHash: call.accessHash, + title: nil, + scheduleTimestamp: nil, + subscribedToScheduled: false, + isStream: false + ), + reference: call.reference, + beginWithVideo: isVideo, + invitePeerIds: [], + endCurrentIfAny: true + ) + })]), on: .root) + } + } else { + strongSelf.sharedContext.mainWindow?.present(textAlertController(context: strongSelf, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_ExternalCallInProgressMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + })]), on: .root) + } + }) + } + } + public func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void) { guard let callResult = self.sharedContext.callManager?.requestCall(context: self, peerId: peerId, isVideo: isVideo, endCurrentIfAny: false) else { return diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 6aa23853da..d07a2bf88d 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2235,15 +2235,43 @@ private func extractAccountManagerState(records: AccountRecordsView deliverOnMainQueue).start(next: { state in - switch state.state { - case .terminated: - callKitIntegration.dropCall(uuid: internalId) - default: - break + let disposable = MetaDisposable() + self.watchedCallsDisposables.add(disposable) + + if let callManager = context.sharedContext.callManager { + let signal = combineLatest(queue: .mainQueue(), context.account.callSessionManager.ringingStates() + |> map { ringingStates -> Bool in + for state in ringingStates { + if state.id == internalId { + return true + } + } + return false + }, + callManager.currentGroupCallSignal + |> map { currentGroupCall -> Bool in + if case let .group(groupCall) = currentGroupCall { + if groupCall.internalId == internalId { + return true + } + } + return false + } + ) + |> mapToSignal { exists0, exists1 -> Signal in + if !(exists0 || exists1) { + return .single(Void()) + |> delay(1.0, queue: .mainQueue()) + } + return .never() } - }))*/ + + disposable.set((signal + |> take(1) + |> deliverOnMainQueue).startStrict(next: { _ in + callKitIntegration.dropCall(uuid: internalId) + })) + } processed = true @@ -2266,6 +2294,7 @@ private func extractAccountManagerState(records: AccountRecordsView Signal { return self.context.get() |> mapToSignal { context -> Signal in diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 068d189a8a..6657465b55 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2883,54 +2883,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - var action: TelegramMediaAction? - for media in message.media { - if let media = media as? TelegramMediaAction { - action = media - break - } - } - guard case let .conferenceCall(conferenceCall) = action?.action else { - return - } - - if let currentGroupCallController = self.context.sharedContext.currentGroupCallController as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == conferenceCall.callId { - self.context.sharedContext.navigateToCurrentCall() - return - } - - let signal = self.context.engine.peers.joinCallInvitationInformation(messageId: message.id) - let _ = (signal - |> deliverOnMainQueue).startStandalone(next: { [weak self] resolvedCallLink in - guard let self else { - return - } - self.context.sharedContext.callManager?.joinConferenceCall( - accountContext: self.context, - initialCall: EngineGroupCallDescription( - id: resolvedCallLink.id, - accessHash: resolvedCallLink.accessHash, - title: nil, - scheduleTimestamp: nil, - subscribedToScheduled: false, - isStream: false - ), - reference: .message(id: message.id), - beginWithVideo: conferenceCall.flags.contains(.isVideo), - invitePeerIds: [] - ) - }, error: { [weak self] error in - guard let self else { - return - } - switch error { - case .doesNotExist: - self.context.sharedContext.openCreateGroupCallUI(context: self.context, peerIds: conferenceCall.otherParticipants, parentController: self) - default: - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - self.present(textAlertController(context: self.context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - } - }) + self.joinConferenceCall(message: EngineMessage(message)) }, longTap: { [weak self] action, params in if let self { self.openLinkLongTap(action, params: params) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 10a241d5ee..f33043a036 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -405,7 +405,8 @@ func openResolvedUrlImpl( slug: link, inviter: resolvedCallLink.inviter, members: resolvedCallLink.members, - totalMemberCount: resolvedCallLink.totalMemberCount + totalMemberCount: resolvedCallLink.totalMemberCount, + info: resolvedCallLink )))) }, error: { _ in var elevatedLayout = true diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 97d9a3c054..8fe77c8f74 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2048,7 +2048,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } let openCall: () -> Void = { - context.sharedContext.callManager?.joinConferenceCall( + let _ = context.sharedContext.callManager?.joinConferenceCall( accountContext: context, initialCall: EngineGroupCallDescription( id: call.callInfo.id, @@ -2060,7 +2060,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { ), reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), beginWithVideo: isVideo, - invitePeerIds: peerIds + invitePeerIds: peerIds, + endCurrentIfAny: true ) completion?() }