Conference improvements

This commit is contained in:
Isaac 2025-04-18 17:56:24 +04:00
parent 0d4b8ee2c0
commit 4bbea1f0d0
25 changed files with 506 additions and 219 deletions

View File

@ -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?";

View File

@ -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)
}

View File

@ -579,6 +579,7 @@ public protocol PresentationCallManager: AnyObject {
initialCall: EngineGroupCallDescription,
reference: InternalGroupCallReference,
beginWithVideo: Bool,
invitePeerIds: [EnginePeer.Id]
)
invitePeerIds: [EnginePeer.Id],
endCurrentIfAny: Bool
) -> JoinGroupCallManagerResult
}

View File

@ -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

View File

@ -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
}
}

View File

@ -589,6 +589,8 @@ public final class InviteLinkInviteController: ViewController {
}
return false
}), in: .window(.root))
strongSelf.controller?.dismiss()
}
})
}

View File

@ -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)")
}

View File

@ -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)

View File

@ -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))
}
})
}
}

View File

@ -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
}
}

View File

@ -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:

View File

@ -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<Bool>(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)
}
}
}
}

View File

@ -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:

View File

@ -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))
}

View File

@ -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

View File

@ -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

View File

@ -143,13 +143,15 @@ func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal<E
}
public final class JoinCallLinkInformation {
public let reference: InternalGroupCallReference
public let id: Int64
public let accessHash: Int64
public let inviter: EnginePeer?
public let members: [EnginePeer]
public let totalMemberCount: Int
public init(id: Int64, accessHash: Int64, inviter: EnginePeer?, members: [EnginePeer], totalMemberCount: Int) {
public init(reference: InternalGroupCallReference, id: Int64, accessHash: Int64, inviter: EnginePeer?, members: [EnginePeer], totalMemberCount: Int) {
self.reference = reference
self.id = id
self.accessHash = accessHash
self.inviter = inviter
@ -176,7 +178,7 @@ func _internal_joinCallLinkInformation(_ hash: String, account: Account) -> 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))
}
}

View File

@ -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

View File

@ -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):

View File

@ -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()
}

View File

@ -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<EnginePeer?, NoError>
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

View File

@ -2235,15 +2235,43 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
if context.account.id == accountId {
context.account.callSessionManager.addConferenceInvitationMessages(ids: [(messageId, IncomingConferenceTermporaryExternalInfo(callId: groupCallId, isVideo: isVideo))])
/*disposable.set((context.account.callSessionManager.callState(internalId: internalId)
|> 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<Void, NoError> 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<TelegramAcco
} else {
guard var updateString = payloadJson["updates"] as? String else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "updates is nil")
self.reportFailedIncomingCallKitCall()
completion()
return
}
@ -2277,11 +2306,13 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
}
guard let updateData = Data(base64Encoded: updateString) else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't decode updateData")
self.reportFailedIncomingCallKitCall()
completion()
return
}
guard let callUpdate = AccountStateManager.extractIncomingCallUpdate(data: updateData) else {
Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't extract call update")
self.reportFailedIncomingCallKitCall()
completion()
return
}
@ -2368,6 +2399,33 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
Logger.shared.log("App \(self.episodeId)", "invalidated token for \(type)")
}
private func reportFailedIncomingCallKitCall() {
guard let callKitIntegration = CallKitIntegration.shared else {
return
}
let uuid = CallSessionInternalId()
callKitIntegration.reportIncomingCall(
uuid: uuid,
stableId: Int64.random(in: Int64.min ... Int64.max),
handle: "Unknown",
phoneNumber: nil,
isVideo: false,
displayTitle: "Unknown",
completion: { error in
if let error = error {
if error.domain == "com.apple.CallKit.error.incomingcall" && (error.code == -3 || error.code == 3) {
Logger.shared.log("PresentationCall", "reportFailedIncomingCallKitCall device in DND mode")
} else {
Logger.shared.log("PresentationCall", "reportFailedIncomingCallKitCall error \(error)")
}
}
}
)
Queue.mainQueue().after(1.0, {
callKitIntegration.dropCall(uuid: uuid)
})
}
private func authorizedContext() -> Signal<AuthorizedApplicationContext, NoError> {
return self.context.get()
|> mapToSignal { context -> Signal<AuthorizedApplicationContext, NoError> in

View File

@ -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)

View File

@ -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

View File

@ -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?()
}