Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Mikhail Filimonov 2025-04-08 19:30:19 +04:00
commit 9ea3a38ebb
23 changed files with 643 additions and 134 deletions

View File

@ -1203,6 +1203,8 @@ public protocol SharedAccountContext: AnyObject {
func makeDebugSettingsController(context: AccountContext?) -> ViewController? func makeDebugSettingsController(context: AccountContext?) -> ViewController?
func openCreateGroupCallUI(context: AccountContext, peerIds: [EnginePeer.Id], parentController: ViewController)
func navigateToCurrentCall() func navigateToCurrentCall()
var hasOngoingCall: ValuePromise<Bool> { get } var hasOngoingCall: ValuePromise<Bool> { get }
var immediateHasOngoingCall: Bool { get } var immediateHasOngoingCall: Bool { get }

View File

@ -77,7 +77,7 @@ public enum ContactMultiselectionControllerMode {
} }
} }
case groupCreation case groupCreation(isCall: Bool)
case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool) case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool)
case channelCreation case channelCreation
case chatSelection(ChatSelection) case chatSelection(ChatSelection)
@ -109,8 +109,26 @@ public final class ContactMultiselectionControllerParams {
public let reachedLimit: ((Int32) -> Void)? public let reachedLimit: ((Int32) -> Void)?
public let openProfile: ((EnginePeer) -> Void)? public let openProfile: ((EnginePeer) -> Void)?
public let sendMessage: ((EnginePeer) -> Void)? public let sendMessage: ((EnginePeer) -> Void)?
public let initialSelectedPeers: [EnginePeer]
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String? = nil, mode: ContactMultiselectionControllerMode, options: Signal<[ContactListAdditionalOption], NoError> = .single([]), filters: [ContactListFilter] = [.excludeSelf], onlyWriteable: Bool = false, isGroupInvitation: Bool = false, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? = nil, alwaysEnabled: Bool = false, limit: Int32? = nil, reachedLimit: ((Int32) -> Void)? = nil, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil) { public init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
title: String? = nil,
mode: ContactMultiselectionControllerMode,
options: Signal<[ContactListAdditionalOption], NoError> = .single([]),
filters: [ContactListFilter] = [.excludeSelf],
onlyWriteable: Bool = false,
isGroupInvitation: Bool = false,
isPeerEnabled: ((EnginePeer) -> Bool)? = nil,
attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? = nil,
alwaysEnabled: Bool = false,
limit: Int32? = nil,
reachedLimit: ((Int32) -> Void)? = nil,
openProfile: ((EnginePeer) -> Void)? = nil,
sendMessage: ((EnginePeer) -> Void)? = nil,
initialSelectedPeers: [EnginePeer] = []
) {
self.context = context self.context = context
self.updatedPresentationData = updatedPresentationData self.updatedPresentationData = updatedPresentationData
self.title = title self.title = title
@ -126,6 +144,7 @@ public final class ContactMultiselectionControllerParams {
self.reachedLimit = reachedLimit self.reachedLimit = reachedLimit
self.openProfile = openProfile self.openProfile = openProfile
self.sendMessage = sendMessage self.sendMessage = sendMessage
self.initialSelectedPeers = initialSelectedPeers
} }
} }

View File

@ -224,6 +224,7 @@ public struct PresentationGroupCallState: Equatable {
public var subscribedToScheduled: Bool public var subscribedToScheduled: Bool
public var isVideoEnabled: Bool public var isVideoEnabled: Bool
public var isVideoWatchersLimitReached: Bool public var isVideoWatchersLimitReached: Bool
public var isMyVideoActive: Bool
public init( public init(
myPeerId: EnginePeer.Id, myPeerId: EnginePeer.Id,
@ -238,7 +239,8 @@ public struct PresentationGroupCallState: Equatable {
scheduleTimestamp: Int32?, scheduleTimestamp: Int32?,
subscribedToScheduled: Bool, subscribedToScheduled: Bool,
isVideoEnabled: Bool, isVideoEnabled: Bool,
isVideoWatchersLimitReached: Bool isVideoWatchersLimitReached: Bool,
isMyVideoActive: Bool
) { ) {
self.myPeerId = myPeerId self.myPeerId = myPeerId
self.networkState = networkState self.networkState = networkState
@ -253,6 +255,7 @@ public struct PresentationGroupCallState: Equatable {
self.subscribedToScheduled = subscribedToScheduled self.subscribedToScheduled = subscribedToScheduled
self.isVideoEnabled = isVideoEnabled self.isVideoEnabled = isVideoEnabled
self.isVideoWatchersLimitReached = isVideoWatchersLimitReached self.isVideoWatchersLimitReached = isVideoWatchersLimitReached
self.isMyVideoActive = isMyVideoActive
} }
} }
@ -569,6 +572,7 @@ public protocol PresentationCallManager: AnyObject {
accountContext: AccountContext, accountContext: AccountContext,
initialCall: EngineGroupCallDescription, initialCall: EngineGroupCallDescription,
reference: InternalGroupCallReference, reference: InternalGroupCallReference,
beginWithVideo: Bool beginWithVideo: Bool,
invitePeerIds: [EnginePeer.Id]
) )
} }

View File

@ -208,7 +208,7 @@ public final class CallListController: TelegramBaseController {
} }
} }
private func createGroupCall() { private func createGroupCall(peerIds: [EnginePeer.Id], completion: (() -> Void)? = nil) {
self.view.endEditing(true) self.view.endEditing(true)
guard !self.presentAccountFrozenInfoIfNeeded() else { guard !self.presentAccountFrozenInfoIfNeeded() else {
@ -274,38 +274,44 @@ public final class CallListController: TelegramBaseController {
isStream: false isStream: false
), ),
reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash), reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash),
beginWithVideo: false beginWithVideo: false,
invitePeerIds: peerIds
) )
completion?()
} }
let controller = InviteLinkInviteController( if !peerIds.isEmpty {
context: self.context, openCall()
updatedPresentationData: nil, } else {
mode: .groupCall(InviteLinkInviteController.Mode.GroupCall(callId: call.callInfo.id, accessHash: call.callInfo.accessHash, isRecentlyCreated: true, canRevoke: true)), let controller = InviteLinkInviteController(
initialInvite: .link(link: call.link, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: self.context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil), context: self.context,
parentNavigationController: self.navigationController as? NavigationController, updatedPresentationData: nil,
completed: { [weak self] result in mode: .groupCall(InviteLinkInviteController.Mode.GroupCall(callId: call.callInfo.id, accessHash: call.callInfo.accessHash, isRecentlyCreated: true, canRevoke: true)),
guard let self else { initialInvite: .link(link: call.link, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: self.context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil),
return parentNavigationController: self.navigationController as? NavigationController,
} completed: { [weak self] result in
if let result { guard let self else {
switch result { return
case .linkCopied: }
//TODO:localize if let result {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } switch result {
self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: "View Call", timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in case .linkCopied:
if case .undo = action { //TODO:localize
openCall() let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
} self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: "View Call", timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
return false if case .undo = action {
}), in: .window(.root)) openCall()
case .openCall: }
openCall() return false
}), in: .window(.root))
case .openCall:
openCall()
}
} }
} }
} )
) self.present(controller, in: .window(.root), with: nil)
self.present(controller, in: .window(.root), with: nil) }
}) })
} }
@ -395,7 +401,7 @@ public final class CallListController: TelegramBaseController {
} }
}, createGroupCall: { [weak self] in }, createGroupCall: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.createGroupCall() strongSelf.createGroupCall(peerIds: [])
} }
}) })
@ -508,21 +514,69 @@ public final class CallListController: TelegramBaseController {
guard !self.presentAccountFrozenInfoIfNeeded() else { guard !self.presentAccountFrozenInfoIfNeeded() else {
return return
} }
let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: self.context, title: { $0.Calls_NewCall }, displayCallIcons: true))
//TODO:localize
let options = [ContactListAdditionalOption(title: "New Call Link", icon: .generic(PresentationResourcesItemList.linkIcon(presentationData.theme)!), action: { [weak self] in
guard let self else {
return
}
self.createGroupCall(peerIds: [])
}, clearHighlightAutomatically: true)]
let controller = self.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(
context: self.context,
title: self.presentationData.strings.Calls_NewCall,
mode: .groupCreation(isCall: true),
options: .single(options),
filters: [.excludeSelf],
onlyWriteable: true,
isGroupInvitation: false,
isPeerEnabled: nil,
attemptDisabledItemSelection: nil,
alwaysEnabled: false,
limit: nil,
reachedLimit: nil,
openProfile: nil,
sendMessage: nil
))
controller.navigationPresentation = .modal controller.navigationPresentation = .modal
self.createActionDisposable.set((controller.result if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(controller)
}
let _ = (controller.result
|> take(1) |> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak controller, weak self] result in |> deliverOnMainQueue).startStandalone(next: { [weak controller, weak self] result in
controller?.dismissSearch() guard let self else {
if let strongSelf = self, let (contactPeers, action, _, _, _, _) = result, let contactPeer = contactPeers.first, case let .peer(peer, _, _) = contactPeer { controller?.dismiss()
strongSelf.call(peer.id, isVideo: action == .videoCall, began: { return
}
guard case let .result(rawPeerIds, _) = result else {
controller?.dismiss()
return
}
let peerIds = rawPeerIds.compactMap { id -> EnginePeer.Id? in
if case let .peer(id) = id {
return id
}
return nil
}
if peerIds.isEmpty {
controller?.dismiss()
return
}
if peerIds.count == 1 {
//TODO:release isVideo
controller?.dismiss()
self.call(peerIds[0], isVideo: false, began: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
let _ = (strongSelf.context.sharedContext.hasOngoingCall.get() let _ = (strongSelf.context.sharedContext.hasOngoingCall.get()
|> filter { $0 } |> filter { $0 }
|> timeout(1.0, queue: Queue.mainQueue(), alternate: .single(true)) |> timeout(1.0, queue: Queue.mainQueue(), alternate: .single(true))
|> delay(0.5, queue: Queue.mainQueue()) |> delay(0.5, queue: Queue.mainQueue())
|> take(1) |> take(1)
|> deliverOnMainQueue).startStandalone(next: { _ in |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in
if let _ = self, let controller = controller, let navigationController = controller.navigationController as? NavigationController { if let _ = self, let controller = controller, let navigationController = controller.navigationController as? NavigationController {
if navigationController.viewControllers.last === controller { if navigationController.viewControllers.last === controller {
let _ = navigationController.popViewController(animated: true) let _ = navigationController.popViewController(animated: true)
@ -531,11 +585,12 @@ public final class CallListController: TelegramBaseController {
}) })
} }
}) })
} else {
self.createGroupCall(peerIds: peerIds, completion: {
controller?.dismiss()
})
} }
})) })
if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(controller)
}
} }
private func presentAccountFrozenInfoIfNeeded(delay: Bool = false) -> Bool { private func presentAccountFrozenInfoIfNeeded(delay: Bool = false) -> Bool {
@ -612,8 +667,8 @@ public final class CallListController: TelegramBaseController {
return return
} }
self.peerViewDisposable.set((self.context.account.viewTracker.peerView(peerId) self.peerViewDisposable.set((self.context.account.viewTracker.peerView(peerId)
|> take(1) |> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] view in |> deliverOnMainQueue).startStrict(next: { [weak self] view in
if let strongSelf = self { if let strongSelf = self {
guard let peer = peerViewMainPeer(view) else { guard let peer = peerViewMainPeer(view) else {
return return
@ -621,7 +676,6 @@ public final class CallListController: TelegramBaseController {
if let cachedUserData = view.cachedData as? CachedUserData, cachedUserData.callsPrivate { if let cachedUserData = view.cachedData as? CachedUserData, cachedUserData.callsPrivate {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_ConnectionErrorTitle, text: presentationData.strings.Call_PrivacyErrorMessage(EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_ConnectionErrorTitle, text: presentationData.strings.Call_PrivacyErrorMessage(EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return return
} }
@ -645,6 +699,7 @@ public final class CallListController: TelegramBaseController {
return return
} }
if conferenceCall.duration != nil { if conferenceCall.duration != nil {
self.context.sharedContext.openCreateGroupCallUI(context: self.context, peerIds: conferenceCall.otherParticipants, parentController: self)
return return
} }
@ -670,8 +725,21 @@ public final class CallListController: TelegramBaseController {
isStream: false isStream: false
), ),
reference: .message(id: message.id), reference: .message(id: message.id),
beginWithVideo: conferenceCall.flags.contains(.isVideo) 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 }
//TODO:localize
self.present(textAlertController(context: self.context, title: nil, text: "An error occurred", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}) })
} }

View File

@ -6,9 +6,9 @@ import TelegramPresentationData
public struct CounterControllerTitle: Equatable { public struct CounterControllerTitle: Equatable {
public var title: String public var title: String
public var counter: String public var counter: String?
public init(title: String, counter: String) { public init(title: String, counter: String?) {
self.title = title self.title = title
self.counter = counter self.counter = counter
} }
@ -18,7 +18,7 @@ public final class CounterControllerTitleView: UIView {
private let titleNode: ImmediateTextNode private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode
public var title: CounterControllerTitle = CounterControllerTitle(title: "", counter: "") { public var title: CounterControllerTitle = CounterControllerTitle(title: "", counter: nil) {
didSet { didSet {
if self.title != oldValue { if self.title != oldValue {
self.update() self.update()
@ -59,7 +59,7 @@ public final class CounterControllerTitleView: UIView {
let primaryTextColor = self.primaryTextColor ?? self.theme.rootController.navigationBar.primaryTextColor let primaryTextColor = self.primaryTextColor ?? self.theme.rootController.navigationBar.primaryTextColor
let secondaryTextColor = self.secondaryTextColor ?? self.theme.rootController.navigationBar.secondaryTextColor let secondaryTextColor = self.secondaryTextColor ?? self.theme.rootController.navigationBar.secondaryTextColor
self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: primaryTextColor) self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.with(size: 13.0, traits: .monospacedNumbers), textColor: secondaryTextColor) self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter ?? "", font: Font.with(size: 13.0, traits: .monospacedNumbers), textColor: secondaryTextColor)
self.accessibilityLabel = self.title.title self.accessibilityLabel = self.title.title
self.accessibilityValue = self.title.counter self.accessibilityValue = self.title.counter
@ -103,7 +103,13 @@ public final class CounterControllerTitleView: UIView {
let titleSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width), height: size.height)) let titleSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width), height: size.height))
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: max(1.0, size.width), height: size.height)) let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: max(1.0, size.width), height: size.height))
let combinedHeight = titleSize.height + subtitleSize.height + spacing
let combinedHeight: CGFloat
if self.title.counter != nil {
combinedHeight = titleSize.height + subtitleSize.height + spacing
} else {
combinedHeight = titleSize.height
}
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
self.titleNode.frame = titleFrame self.titleNode.frame = titleFrame

View File

@ -427,7 +427,7 @@ public final class InviteLinkInviteController: ViewController {
strongSelf.controller?.present(controller, in: .window(.root)) strongSelf.controller?.present(controller, in: .window(.root))
}) })
} else if case .groupCall = self.mode { } else if case .groupCall = self.mode {
let controller = QrCodeScreen(context: context, updatedPresentationData: (self.presentationData, self.presentationDataPromise.get()), subject: .invite(invite: invite, type: .channel)) let controller = QrCodeScreen(context: context, updatedPresentationData: (self.presentationData, self.presentationDataPromise.get()), subject: .invite(invite: invite, type: .groupCall))
self.controller?.present(controller, in: .window(.root)) self.controller?.present(controller, in: .window(.root))
} }
} }

View File

@ -1048,14 +1048,10 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present
entries.append(.settingInfo(presentationData.theme, settingInfoText, settingInfoLink)) entries.append(.settingInfo(presentationData.theme, settingInfoText, settingInfoLink))
} }
if case .phoneNumber = kind, state.setting == .nobody { entries.append(.phoneDiscoveryHeader(presentationData.theme, presentationData.strings.PrivacyPhoneNumberSettings_DiscoveryHeader))
if state.phoneDiscoveryEnabled == false || phoneNumber.hasPrefix("888") { entries.append(.phoneDiscoveryEverybody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenEverybody, state.phoneDiscoveryEnabled != false))
entries.append(.phoneDiscoveryHeader(presentationData.theme, presentationData.strings.PrivacyPhoneNumberSettings_DiscoveryHeader)) entries.append(.phoneDiscoveryMyContacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.phoneDiscoveryEnabled == false))
entries.append(.phoneDiscoveryEverybody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenEverybody, state.phoneDiscoveryEnabled != false)) entries.append(.phoneDiscoveryInfo(presentationData.theme, state.phoneDiscoveryEnabled != false ? presentationData.strings.PrivacyPhoneNumberSettings_CustomPublicLink("+\(phoneNumber)").string : presentationData.strings.PrivacyPhoneNumberSettings_CustomDisabledHelp, phoneLink))
entries.append(.phoneDiscoveryMyContacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.phoneDiscoveryEnabled == false))
entries.append(.phoneDiscoveryInfo(presentationData.theme, state.phoneDiscoveryEnabled != false ? presentationData.strings.PrivacyPhoneNumberSettings_CustomPublicLink("+\(phoneNumber)").string : presentationData.strings.PrivacyPhoneNumberSettings_CustomDisabledHelp, phoneLink))
}
}
if case .voiceMessages = kind, !isPremium { if case .voiceMessages = kind, !isPremium {

View File

@ -1083,7 +1083,8 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
accountContext: AccountContext, accountContext: AccountContext,
initialCall: EngineGroupCallDescription, initialCall: EngineGroupCallDescription,
reference: InternalGroupCallReference, reference: InternalGroupCallReference,
beginWithVideo: Bool beginWithVideo: Bool,
invitePeerIds: [EnginePeer.Id]
) { ) {
let keyPair: TelegramKeyPair let keyPair: TelegramKeyPair
guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else { guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else {
@ -1109,6 +1110,9 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
beginWithVideo: beginWithVideo, beginWithVideo: beginWithVideo,
sharedAudioContext: nil sharedAudioContext: nil
) )
for peerId in invitePeerIds {
let _ = call.invitePeer(peerId, isVideo: beginWithVideo)
}
self.updateCurrentGroupCall(.group(call)) self.updateCurrentGroupCall(.group(call))
} }
} }

View File

@ -34,7 +34,8 @@ private extension PresentationGroupCallState {
scheduleTimestamp: scheduleTimestamp, scheduleTimestamp: scheduleTimestamp,
subscribedToScheduled: subscribedToScheduled, subscribedToScheduled: subscribedToScheduled,
isVideoEnabled: false, isVideoEnabled: false,
isVideoWatchersLimitReached: false isVideoWatchersLimitReached: false,
isMyVideoActive: false
) )
} }
} }
@ -1708,6 +1709,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
var prioritizeVP8 = false var prioritizeVP8 = false
#if DEBUG
prioritizeVP8 = "".isEmpty
#endif
if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data, let value = data["ios_calls_prioritize_vp8"] as? Double { if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data, let value = data["ios_calls_prioritize_vp8"] as? Double {
prioritizeVP8 = value != 0.0 prioritizeVP8 = value != 0.0
} }
@ -3130,6 +3134,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if let videoCapturer = self.videoCapturer { if let videoCapturer = self.videoCapturer {
self.requestVideo(capturer: videoCapturer) self.requestVideo(capturer: videoCapturer)
var stateValue = self.stateValue
stateValue.isMyVideoActive = true
self.stateValue = stateValue
} }
} }
@ -3152,6 +3160,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
})) }))
self.updateLocalVideoState() self.updateLocalVideoState()
var stateValue = self.stateValue
stateValue.isMyVideoActive = true
self.stateValue = stateValue
} }
} }
@ -3165,6 +3177,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.isVideoMuted = true self.isVideoMuted = true
self.updateLocalVideoState() self.updateLocalVideoState()
var stateValue = self.stateValue
stateValue.isMyVideoActive = false
self.stateValue = stateValue
} }
} }

View File

@ -33,11 +33,13 @@ final class VideoChatActionButtonComponent: Component {
case audio(audio: Audio) case audio(audio: Audio)
case video case video
case rotateCamera
case leave case leave
} }
case audio(audio: Audio, isEnabled: Bool) case audio(audio: Audio, isEnabled: Bool)
case video(isActive: Bool) case video(isActive: Bool)
case rotateCamera
case leave case leave
fileprivate var iconType: IconType { fileprivate var iconType: IconType {
@ -55,6 +57,8 @@ final class VideoChatActionButtonComponent: Component {
return .audio(audio: mappedAudio) return .audio(audio: mappedAudio)
case .video: case .video:
return .video return .video
case .rotateCamera:
return .rotateCamera
case .leave: case .leave:
return .leave return .leave
} }
@ -176,6 +180,19 @@ final class VideoChatActionButtonComponent: Component {
backgroundColor = UIColor(rgb: 0x3252EF) backgroundColor = UIColor(rgb: 0x3252EF)
} }
iconDiameter = 60.0 iconDiameter = 60.0
case .rotateCamera:
titleText = ""
switch component.microphoneState {
case .connecting:
backgroundColor = UIColor(white: 0.1, alpha: 1.0)
case .muted:
backgroundColor = UIColor(rgb: 0x027FFF)
case .unmuted:
backgroundColor = UIColor(rgb: 0x34C659)
case .raiseHand, .scheduled:
backgroundColor = UIColor(rgb: 0x3252EF)
}
iconDiameter = 60.0
case .leave: case .leave:
titleText = component.strings.VoiceChat_Leave titleText = component.strings.VoiceChat_Leave
backgroundColor = UIColor(rgb: 0x47191E) backgroundColor = UIColor(rgb: 0x47191E)
@ -206,6 +223,8 @@ final class VideoChatActionButtonComponent: Component {
self.contentImage = UIImage(bundleImageName: iconName)?.precomposed().withRenderingMode(.alwaysTemplate) self.contentImage = UIImage(bundleImageName: iconName)?.precomposed().withRenderingMode(.alwaysTemplate)
case .video: case .video:
self.contentImage = UIImage(bundleImageName: "Call/CallCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate) self.contentImage = UIImage(bundleImageName: "Call/CallCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate)
case .rotateCamera:
self.contentImage = UIImage(bundleImageName: "Call/CallSwitchCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate)
case .leave: case .leave:
self.contentImage = generateImage(CGSize(width: 28.0, height: 28.0), opaque: false, rotatedContext: { size, context in self.contentImage = generateImage(CGSize(width: 28.0, height: 28.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size) let bounds = CGRect(origin: CGPoint(), size: size)
@ -277,8 +296,10 @@ final class VideoChatActionButtonComponent: Component {
if iconView.superview == nil { if iconView.superview == nil {
self.addSubview(iconView) self.addSubview(iconView)
} }
transition.setFrame(view: iconView, frame: iconFrame) transition.setPosition(view: iconView, position: iconFrame.center)
transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
transition.setAlpha(view: iconView, alpha: isEnabled ? 1.0 : 0.6) transition.setAlpha(view: iconView, alpha: isEnabled ? 1.0 : 0.6)
transition.setScale(view: iconView, scale: availableSize.height / 56.0)
} }
self.isEnabled = isEnabled self.isEnabled = isEnabled

View File

@ -227,6 +227,7 @@ final class VideoChatScreenComponent: Component {
var isEncryptionKeyExpanded: Bool = false var isEncryptionKeyExpanded: Bool = false
let videoButton = ComponentView<Empty>() let videoButton = ComponentView<Empty>()
let videoControlButton = ComponentView<Empty>()
let leaveButton = ComponentView<Empty>() let leaveButton = ComponentView<Empty>()
let microphoneButton = ComponentView<Empty>() let microphoneButton = ComponentView<Empty>()
@ -1130,7 +1131,8 @@ final class VideoChatScreenComponent: Component {
scheduleTimestamp: nil, scheduleTimestamp: nil,
subscribedToScheduled: false, subscribedToScheduled: false,
isVideoEnabled: true, isVideoEnabled: true,
isVideoWatchersLimitReached: false isVideoWatchersLimitReached: false,
isMyVideoActive: false
) )
return .single((callState, invitedPeers.compactMap({ peer -> VideoChatScreenComponent.InvitedPeer? in return .single((callState, invitedPeers.compactMap({ peer -> VideoChatScreenComponent.InvitedPeer? in
@ -2657,38 +2659,84 @@ final class VideoChatScreenComponent: Component {
} }
let videoButtonContent: VideoChatActionButtonComponent.Content let videoButtonContent: VideoChatActionButtonComponent.Content
if let callState = self.callState, let muteState = callState.muteState, !muteState.canUnmute { let videoControlButtonContent: VideoChatActionButtonComponent.Content
var buttonAudio: VideoChatActionButtonComponent.Content.Audio = .speaker
var buttonIsEnabled = false var buttonAudio: VideoChatActionButtonComponent.Content.Audio = .speaker
if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput { var buttonIsEnabled = false
buttonIsEnabled = availableOutputs.count > 1 if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput {
switch currentOutput { buttonIsEnabled = availableOutputs.count > 1
case .builtin: switch currentOutput {
buttonAudio = .builtin case .builtin:
case .speaker: buttonAudio = .builtin
buttonAudio = .speaker case .speaker:
case .headphones: buttonAudio = .speaker
buttonAudio = .headphones case .headphones:
case let .port(port): buttonAudio = .headphones
var type: VideoChatActionButtonComponent.Content.BluetoothType = .generic case let .port(port):
let portName = port.name.lowercased() var type: VideoChatActionButtonComponent.Content.BluetoothType = .generic
if portName.contains("airpods max") { let portName = port.name.lowercased()
type = .airpodsMax if portName.contains("airpods max") {
} else if portName.contains("airpods pro") { type = .airpodsMax
type = .airpodsPro } else if portName.contains("airpods pro") {
} else if portName.contains("airpods") { type = .airpodsPro
type = .airpods } else if portName.contains("airpods") {
} type = .airpods
buttonAudio = .bluetooth(type)
}
if availableOutputs.count <= 1 {
buttonAudio = .none
} }
buttonAudio = .bluetooth(type)
}
if availableOutputs.count <= 1 {
buttonAudio = .none
} }
videoButtonContent = .audio(audio: buttonAudio, isEnabled: buttonIsEnabled)
} else {
videoButtonContent = .video(isActive: false)
} }
if let callState = self.callState, let muteState = callState.muteState, !muteState.canUnmute {
videoButtonContent = .audio(audio: buttonAudio, isEnabled: buttonIsEnabled)
videoControlButtonContent = .audio(audio: buttonAudio, isEnabled: buttonIsEnabled)
} else {
let isVideoActive = self.callState?.isMyVideoActive ?? false
videoButtonContent = .video(isActive: isVideoActive)
if isVideoActive {
videoControlButtonContent = .rotateCamera
} else {
videoControlButtonContent = .audio(audio: buttonAudio, isEnabled: buttonIsEnabled)
}
}
var displayVideoControlButton = true
if areButtonsCollapsed {
displayVideoControlButton = false
} else if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, !expandedParticipantsVideoState.isUIHidden {
displayVideoControlButton = false
}
let videoControlButtonSize = self.videoControlButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(VideoChatActionButtonComponent(
strings: environment.strings,
content: videoControlButtonContent,
microphoneState: actionButtonMicrophoneState,
isCollapsed: true
)),
effectAlignment: .center,
action: { [weak self] in
guard let self else {
return
}
if let state = self.callState, state.isMyVideoActive {
if case let .group(groupCall) = self.currentCall {
groupCall.switchVideoCamera()
}
} else {
self.onAudioRoutePressed()
}
},
animateAlpha: false
)),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
let _ = self.videoButton.update( let _ = self.videoButton.update(
transition: transition, transition: transition,
component: AnyComponent(PlainButtonComponent( component: AnyComponent(PlainButtonComponent(
@ -2714,12 +2762,33 @@ final class VideoChatScreenComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: actionButtonDiameter, height: actionButtonDiameter) containerSize: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)
) )
let videoControlButtonSpacing: CGFloat = 8.0
var videoButtonFrame = leftActionButtonFrame
if displayVideoControlButton {
let totalVideoButtonsHeight = actionButtonDiameter + videoControlButtonSpacing + videoControlButtonSize.height
videoButtonFrame.origin.y = videoButtonFrame.minY + floor((videoButtonFrame.height - totalVideoButtonsHeight) / 2.0) + videoControlButtonSpacing + videoControlButtonSize.height
}
let videoControlButtonFrame = CGRect(origin: CGPoint(x: videoButtonFrame.minX + floor((videoButtonFrame.width - videoControlButtonSize.width) / 2.0), y: videoButtonFrame.minY - videoControlButtonSpacing - videoControlButtonSize.height), size: videoControlButtonSize)
if let videoControlButtonView = self.videoControlButton.view {
if videoControlButtonView.superview == nil {
self.containerView.addSubview(videoControlButtonView)
}
transition.setPosition(view: videoControlButtonView, position: videoControlButtonFrame.center)
transition.setBounds(view: videoControlButtonView, bounds: CGRect(origin: CGPoint(), size: videoControlButtonFrame.size))
alphaTransition.setAlpha(view: videoControlButtonView, alpha: displayVideoControlButton ? 1.0 : 0.0)
transition.setScale(view: videoControlButtonView, scale: displayVideoControlButton ? 1.0 : 0.001)
}
if let videoButtonView = self.videoButton.view { if let videoButtonView = self.videoButton.view {
if videoButtonView.superview == nil { if videoButtonView.superview == nil {
self.containerView.addSubview(videoButtonView) self.containerView.addSubview(videoButtonView)
} }
transition.setPosition(view: videoButtonView, position: leftActionButtonFrame.center) transition.setPosition(view: videoButtonView, position: videoButtonFrame.center)
transition.setBounds(view: videoButtonView, bounds: CGRect(origin: CGPoint(), size: leftActionButtonFrame.size)) transition.setBounds(view: videoButtonView, bounds: CGRect(origin: CGPoint(), size: videoButtonFrame.size))
} }
let _ = self.leaveButton.update( let _ = self.leaveButton.update(

View File

@ -28,6 +28,7 @@ public struct UserLimitsConfiguration: Equatable {
public var maxGiveawayCountriesCount: Int32 public var maxGiveawayCountriesCount: Int32
public var maxGiveawayPeriodSeconds: Int32 public var maxGiveawayPeriodSeconds: Int32
public var maxChannelRecommendationsCount: Int32 public var maxChannelRecommendationsCount: Int32
public var maxConferenceParticipantCount: Int32
public static var defaultValue: UserLimitsConfiguration { public static var defaultValue: UserLimitsConfiguration {
return UserLimitsConfiguration( return UserLimitsConfiguration(
@ -56,7 +57,8 @@ public struct UserLimitsConfiguration: Equatable {
maxGiveawayChannelsCount: 10, maxGiveawayChannelsCount: 10,
maxGiveawayCountriesCount: 10, maxGiveawayCountriesCount: 10,
maxGiveawayPeriodSeconds: 86400 * 31, maxGiveawayPeriodSeconds: 86400 * 31,
maxChannelRecommendationsCount: 10 maxChannelRecommendationsCount: 10,
maxConferenceParticipantCount: 100
) )
} }
@ -86,7 +88,8 @@ public struct UserLimitsConfiguration: Equatable {
maxGiveawayChannelsCount: Int32, maxGiveawayChannelsCount: Int32,
maxGiveawayCountriesCount: Int32, maxGiveawayCountriesCount: Int32,
maxGiveawayPeriodSeconds: Int32, maxGiveawayPeriodSeconds: Int32,
maxChannelRecommendationsCount: Int32 maxChannelRecommendationsCount: Int32,
maxConferenceParticipantCount: Int32
) { ) {
self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedChatCount = maxPinnedChatCount
self.maxPinnedSavedChatCount = maxPinnedSavedChatCount self.maxPinnedSavedChatCount = maxPinnedSavedChatCount
@ -114,6 +117,7 @@ public struct UserLimitsConfiguration: Equatable {
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds
self.maxChannelRecommendationsCount = maxChannelRecommendationsCount self.maxChannelRecommendationsCount = maxChannelRecommendationsCount
self.maxConferenceParticipantCount = maxConferenceParticipantCount
} }
} }
@ -167,5 +171,6 @@ extension UserLimitsConfiguration {
self.maxGiveawayCountriesCount = getGeneralValue("giveaway_countries_max", orElse: defaultValue.maxGiveawayCountriesCount) self.maxGiveawayCountriesCount = getGeneralValue("giveaway_countries_max", orElse: defaultValue.maxGiveawayCountriesCount)
self.maxGiveawayPeriodSeconds = getGeneralValue("giveaway_period_max", orElse: defaultValue.maxGiveawayPeriodSeconds) self.maxGiveawayPeriodSeconds = getGeneralValue("giveaway_period_max", orElse: defaultValue.maxGiveawayPeriodSeconds)
self.maxChannelRecommendationsCount = getValue("recommended_channels_limit", orElse: defaultValue.maxChannelRecommendationsCount) self.maxChannelRecommendationsCount = getValue("recommended_channels_limit", orElse: defaultValue.maxChannelRecommendationsCount)
self.maxConferenceParticipantCount = getGeneralValue("conference_call_size_limit", orElse: defaultValue.maxConferenceParticipantCount)
} }
} }

View File

@ -62,6 +62,7 @@ public enum EngineConfiguration {
public let maxGiveawayCountriesCount: Int32 public let maxGiveawayCountriesCount: Int32
public let maxGiveawayPeriodSeconds: Int32 public let maxGiveawayPeriodSeconds: Int32
public let maxChannelRecommendationsCount: Int32 public let maxChannelRecommendationsCount: Int32
public let maxConferenceParticipantCount: Int32
public static var defaultValue: UserLimits { public static var defaultValue: UserLimits {
return UserLimits(UserLimitsConfiguration.defaultValue) return UserLimits(UserLimitsConfiguration.defaultValue)
@ -93,7 +94,8 @@ public enum EngineConfiguration {
maxGiveawayChannelsCount: Int32, maxGiveawayChannelsCount: Int32,
maxGiveawayCountriesCount: Int32, maxGiveawayCountriesCount: Int32,
maxGiveawayPeriodSeconds: Int32, maxGiveawayPeriodSeconds: Int32,
maxChannelRecommendationsCount: Int32 maxChannelRecommendationsCount: Int32,
maxConferenceParticipantCount: Int32
) { ) {
self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedChatCount = maxPinnedChatCount
self.maxPinnedSavedChatCount = maxPinnedSavedChatCount self.maxPinnedSavedChatCount = maxPinnedSavedChatCount
@ -121,6 +123,7 @@ public enum EngineConfiguration {
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds
self.maxChannelRecommendationsCount = maxChannelRecommendationsCount self.maxChannelRecommendationsCount = maxChannelRecommendationsCount
self.maxConferenceParticipantCount = maxConferenceParticipantCount
} }
} }
} }
@ -183,7 +186,8 @@ public extension EngineConfiguration.UserLimits {
maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount, maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount,
maxGiveawayCountriesCount: userLimitsConfiguration.maxGiveawayCountriesCount, maxGiveawayCountriesCount: userLimitsConfiguration.maxGiveawayCountriesCount,
maxGiveawayPeriodSeconds: userLimitsConfiguration.maxGiveawayPeriodSeconds, maxGiveawayPeriodSeconds: userLimitsConfiguration.maxGiveawayPeriodSeconds,
maxChannelRecommendationsCount: userLimitsConfiguration.maxChannelRecommendationsCount maxChannelRecommendationsCount: userLimitsConfiguration.maxChannelRecommendationsCount,
maxConferenceParticipantCount: userLimitsConfiguration.maxConferenceParticipantCount
) )
} }
} }

View File

@ -178,17 +178,23 @@ func _internal_joinCallLinkInformation(_ hash: String, account: Account) -> Sign
} }
} }
func _internal_joinCallInvitationInformation(account: Account, messageId: MessageId) -> Signal<JoinCallLinkInformation, JoinLinkInfoError> { public enum JoinCallLinkInfoError {
case generic
case flood
case doesNotExist
}
func _internal_joinCallInvitationInformation(account: Account, messageId: MessageId) -> Signal<JoinCallLinkInformation, JoinCallLinkInfoError> {
return _internal_getCurrentGroupCall(account: account, reference: .message(id: messageId)) return _internal_getCurrentGroupCall(account: account, reference: .message(id: messageId))
|> mapError { error -> JoinLinkInfoError in |> mapError { error -> JoinCallLinkInfoError in
switch error { switch error {
case .generic: case .generic:
return .generic return .generic
} }
} }
|> mapToSignal { call -> Signal<JoinCallLinkInformation, JoinLinkInfoError> in |> mapToSignal { call -> Signal<JoinCallLinkInformation, JoinCallLinkInfoError> in
guard let call = call else { guard let call else {
return .fail(.generic) return .fail(.doesNotExist)
} }
var members: [EnginePeer] = [] var members: [EnginePeer] = []
for participant in call.topParticipants { for participant in call.topParticipants {

View File

@ -842,7 +842,7 @@ public extension TelegramEngine {
return _internal_joinCallLinkInformation(hash, account: self.account) return _internal_joinCallLinkInformation(hash, account: self.account)
} }
public func joinCallInvitationInformation(messageId: EngineMessage.Id) -> Signal<JoinCallLinkInformation, JoinLinkInfoError> { public func joinCallInvitationInformation(messageId: EngineMessage.Id) -> Signal<JoinCallLinkInformation, JoinCallLinkInfoError> {
return _internal_joinCallInvitationInformation(account: self.account, messageId: messageId) return _internal_joinCallInvitationInformation(account: self.account, messageId: messageId)
} }

View File

@ -469,6 +469,9 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen", "//submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen",
"//submodules/TelegramUI/Components/JoinSubjectScreen", "//submodules/TelegramUI/Components/JoinSubjectScreen",
"//submodules/TelegramUI/Components/Chat/QuickShareScreen", "//submodules/TelegramUI/Components/Chat/QuickShareScreen",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/TelegramUI/Components/CheckComponent",
"//third-party/recaptcha:RecaptchaEnterprise", "//third-party/recaptcha:RecaptchaEnterprise",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -114,7 +114,6 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
var callDuration: Int32? var callDuration: Int32?
var callSuccessful = true var callSuccessful = true
var isVideo = false var isVideo = false
var hasCallButton = true
var updateConferenceTimerEndTimeout: Int32? var updateConferenceTimerEndTimeout: Int32?
for media in item.message.media { for media in item.message.media {
if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action { if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action {
@ -173,9 +172,7 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
#else #else
missedTimeout = 30 missedTimeout = 30
#endif #endif
if conferenceCall.duration != nil {
hasCallButton = false
}
let currentTime = Int32(Date().timeIntervalSince1970) let currentTime = Int32(Date().timeIntervalSince1970)
if conferenceCall.flags.contains(.isMissed) { if conferenceCall.flags.contains(.isMissed) {
titleString = "Declined Group Call" titleString = "Declined Group Call"
@ -296,9 +293,7 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
if hasCallButton { boundingSize.width += 54.0
boundingSize.width += 54.0
}
return (boundingSize.width, { boundingWidth in return (boundingSize.width, { boundingWidth in
return (boundingSize, { [weak self] animation, _, _ in return (boundingSize, { [weak self] animation, _, _ in
@ -359,7 +354,6 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
if let buttonImage = buttonImage { if let buttonImage = buttonImage {
strongSelf.buttonNode.setImage(buttonImage, for: []) strongSelf.buttonNode.setImage(buttonImage, for: [])
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: boundingWidth - buttonImage.size.width - 8.0, y: 15.0), size: buttonImage.size) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: boundingWidth - buttonImage.size.width - 8.0, y: 15.0), size: buttonImage.size)
strongSelf.buttonNode.isHidden = !hasCallButton
} }
if let activeConferenceUpdateTimer = strongSelf.activeConferenceUpdateTimer { if let activeConferenceUpdateTimer = strongSelf.activeConferenceUpdateTimer {
@ -411,10 +405,6 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
} }
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.isHidden {
return ChatMessageBubbleContentTapAction(content: .none)
}
if self.buttonNode.frame.contains(point) { if self.buttonNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore) return ChatMessageBubbleContentTapAction(content: .ignore)
} else if self.bounds.contains(point), let item = self.item { } else if self.bounds.contains(point), let item = self.item {

View File

@ -405,7 +405,8 @@ private final class JoinSubjectScreenComponent: Component {
isStream: false isStream: false
), ),
reference: .link(slug: groupCall.slug), reference: .link(slug: groupCall.slug),
beginWithVideo: false beginWithVideo: false,
invitePeerIds: []
) )
self.environment?.controller()?.dismiss() self.environment?.controller()?.dismiss()

View File

@ -2894,6 +2894,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return return
} }
if conferenceCall.duration != nil { if conferenceCall.duration != nil {
self.context.sharedContext.openCreateGroupCallUI(context: self.context, peerIds: conferenceCall.otherParticipants, parentController: self)
return return
} }
@ -2919,8 +2920,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
isStream: false isStream: false
), ),
reference: .message(id: message.id), reference: .message(id: message.id),
beginWithVideo: conferenceCall.flags.contains(.isVideo) 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 }
//TODO:localize
self.present(textAlertController(context: self.context, title: nil, text: "An error occurred", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}) })
}, longTap: { [weak self] action, params in }, longTap: { [weak self] action, params in
if let self { if let self {

View File

@ -125,7 +125,7 @@ public class ComposeControllerImpl: ViewController, ComposeController {
self.contactsNode.openCreateNewGroup = { [weak self] in self.contactsNode.openCreateNewGroup = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
let controller = strongSelf.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: strongSelf.context, mode: .groupCreation, onlyWriteable: true)) let controller = strongSelf.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: strongSelf.context, mode: .groupCreation(isCall: false), onlyWriteable: true))
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller, completion: { [weak self] in (strongSelf.navigationController as? NavigationController)?.pushViewController(controller, completion: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)

View File

@ -241,8 +241,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
} }
switch self.mode { switch self.mode {
case .groupCreation: case let .groupCreation(isCall):
let maxCount: Int32 = self.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 let maxCount: Int32
if isCall {
maxCount = self.context.userLimits.maxConferenceParticipantCount
} else {
maxCount = self.limitsConfiguration?.maxSupergroupMemberCount ?? 5000
}
let count: Int let count: Int
switch self.contactsNode.contentNode { switch self.contactsNode.contentNode {
case let .contacts(contactsNode): case let .contacts(contactsNode):
@ -250,8 +255,12 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
case let .chats(chatsNode): case let .chats(chatsNode):
count = chatsNode.currentState.selectedPeerIds.count count = chatsNode.currentState.selectedPeerIds.count
} }
self.titleView.title = CounterControllerTitle(title: self.params.title ?? self.presentationData.strings.Compose_NewGroupTitle, counter: "\(count)/\(maxCount)") if isCall && count == 0 {
if self.rightNavigationButton == nil { self.titleView.title = CounterControllerTitle(title: self.params.title ?? self.presentationData.strings.Compose_NewGroupTitle, counter: nil)
} else {
self.titleView.title = CounterControllerTitle(title: self.params.title ?? self.presentationData.strings.Compose_NewGroupTitle, counter: "\(count)/\(maxCount)")
}
if self.rightNavigationButton == nil && !isCall {
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
self.rightNavigationButton = rightNavigationButton self.rightNavigationButton = rightNavigationButton
self.navigationItem.rightBarButtonItem = self.rightNavigationButton self.navigationItem.rightBarButtonItem = self.rightNavigationButton
@ -328,7 +337,12 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
var addedToken: EditableTokenListToken? var addedToken: EditableTokenListToken?
var removedTokenId: AnyHashable? var removedTokenId: AnyHashable?
let maxRegularCount: Int32 = strongSelf.limitsConfiguration?.maxGroupMemberCount ?? 200 let maxRegularCount: Int32
if case .groupCreation(true) = strongSelf.mode {
maxRegularCount = strongSelf.context.userLimits.maxConferenceParticipantCount
} else {
maxRegularCount = strongSelf.limitsConfiguration?.maxGroupMemberCount ?? 200
}
var displayCountAlert = false var displayCountAlert = false
var selectionState: ContactListNodeGroupSelectionState? var selectionState: ContactListNodeGroupSelectionState?
@ -349,6 +363,10 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
if updatedState.selectedPeerIndices[.peer(peer.id)] == nil { if updatedState.selectedPeerIndices[.peer(peer.id)] == nil {
removedTokenId = peer.id removedTokenId = peer.id
} else { } else {
var selectedPeerMap = updatedState.selectedPeerMap
selectedPeerMap[.peer(peer.id)] = .peer(peer: peer, isGlobal: false, participantCount: nil)
updatedState = updatedState.withSelectedPeerMap(selectedPeerMap)
if updatedState.selectedPeerIndices.count >= maxRegularCount { if updatedState.selectedPeerIndices.count >= maxRegularCount {
displayCountAlert = true displayCountAlert = true
updatedState = updatedState.withToggledPeerId(.peer(peer.id)) updatedState = updatedState.withToggledPeerId(.peer(peer.id))
@ -415,6 +433,24 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
} }
} }
} }
if !self.params.initialSelectedPeers.isEmpty {
for peer in self.params.initialSelectedPeers {
self.contactsNode.openPeer?(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil))
}
/*if case let .contacts(contactsNode) = self.contactsNode.contentNode {
contactsNode.updateSelectionState { state in
var updatedState = state ?? ContactListNodeGroupSelectionState()
var selectedPeerMap = updatedState.selectedPeerMap
for peer in self.params.initialSelectedPeers {
updatedState = updatedState.withToggledPeerId(.peer(peer.id))
selectedPeerMap[.peer(peer.id)] = .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil)
}
updatedState = updatedState.withSelectedPeerMap(selectedPeerMap)
return updatedState
}
}*/
}
self.contactsNode.openPeerMore = { [weak self] peer, node, gesture in self.contactsNode.openPeerMore = { [weak self] peer, node, gesture in
guard let self, case let .peer(peer, _, _) = peer, let node = node as? ContextReferenceContentNode else { guard let self, case let .peer(peer, _, _) = peer, let node = node as? ContextReferenceContentNode else {
@ -538,8 +574,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
break break
} }
switch strongSelf.mode { switch strongSelf.mode {
case .groupCreation: case let .groupCreation(isCall):
let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 let maxCount: Int32
if isCall {
maxCount = strongSelf.context.userLimits.maxConferenceParticipantCount
} else {
maxCount = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000
}
strongSelf.titleView.title = CounterControllerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)") strongSelf.titleView.title = CounterControllerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)")
case .premiumGifting: case .premiumGifting:
let maxCount: Int32 = strongSelf.limit ?? 10 let maxCount: Int32 = strongSelf.limit ?? 10

View File

@ -14,6 +14,7 @@ import MultiAnimationRenderer
import EditableTokenListNode import EditableTokenListNode
import SolidRoundedButtonNode import SolidRoundedButtonNode
import ContextUI import ContextUI
import ComponentFlow
private struct SearchResultEntry: Identifiable { private struct SearchResultEntry: Identifiable {
let index: Int let index: Int
@ -53,6 +54,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
var searchResultsNode: ContactListNode? var searchResultsNode: ContactListNode?
private let context: AccountContext private let context: AccountContext
private let mode: ContactMultiselectionControllerMode
private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)? private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)?
@ -81,12 +83,15 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
private let isPeerEnabled: ((EnginePeer) -> Bool)? private let isPeerEnabled: ((EnginePeer) -> Bool)?
private let onlyWriteable: Bool private let onlyWriteable: Bool
private let isGroupInvitation: Bool private let isGroupInvitation: Bool
private var bottomPanel: ComponentView<Empty>?
init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) { init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) {
self.navigationBar = navigationBar self.navigationBar = navigationBar
self.context = context self.context = context
self.presentationData = presentationData self.presentationData = presentationData
self.mode = mode
self.animationCache = context.animationCache self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer self.animationRenderer = context.animationRenderer
@ -120,6 +125,17 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: {
proceedImpl?() proceedImpl?()
}) })
case let .groupCreation(isCall):
if isCall {
//TODO:localize
placeholder = "Search for contacts or usernames"
self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: {
proceedImpl?()
})
} else {
placeholder = self.presentationData.strings.Compose_TokenListPlaceholder
self.footerPanelNode = nil
}
default: default:
placeholder = self.presentationData.strings.Compose_TokenListPlaceholder placeholder = self.presentationData.strings.Compose_TokenListPlaceholder
self.footerPanelNode = nil self.footerPanelNode = nil
@ -462,7 +478,24 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
if case let .contacts(contactListNode) = self.contentNode { if case let .contacts(contactListNode) = self.contentNode {
count = contactListNode.selectionState?.selectedPeerIndices.count ?? 0 count = contactListNode.selectionState?.selectedPeerIndices.count ?? 0
} }
footerPanelNode.count = count if case let .groupCreation(isCall) = self.mode, isCall {
//TODO:localize
if count == 0 {
// Don't set anything to prevent state update
} else if count <= 1 {
let callTitle: String
if case let .contacts(contactListNode) = self.contentNode, let peer = contactListNode.selectedPeers.first, case let .peer(peer, _, _) = peer {
callTitle = "Call \(EnginePeer(peer).compactDisplayTitle)"
} else {
callTitle = "Call"
}
footerPanelNode.content = FooterPanelNode.Content(title: callTitle, badge: "")
} else {
footerPanelNode.content = FooterPanelNode.Content(title: "Call", badge: "\(count)")
}
} else {
footerPanelNode.content = FooterPanelNode.Content(title: self.presentationData.strings.Premium_Gift_ContactSelection_Proceed, badge: count == 0 ? "" : "\(count)")
}
let panelHeight = footerPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: headerInsets.bottom, transition: transition) let panelHeight = footerPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: headerInsets.bottom, transition: transition)
if count == 0 { if count == 0 {
transition.updateFrame(node: footerPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: panelHeight))) transition.updateFrame(node: footerPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: panelHeight)))
@ -509,6 +542,16 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
private final class FooterPanelNode: ASDisplayNode { private final class FooterPanelNode: ASDisplayNode {
struct Content: Equatable {
let title: String
let badge: String
init(title: String, badge: String) {
self.title = title
self.badge = badge
}
}
private let theme: PresentationTheme private let theme: PresentationTheme
private let strings: PresentationStrings private let strings: PresentationStrings
@ -517,11 +560,11 @@ private final class FooterPanelNode: ASDisplayNode {
private var validLayout: (CGFloat, CGFloat, CGFloat)? private var validLayout: (CGFloat, CGFloat, CGFloat)?
var count: Int = 0 { var content: Content {
didSet { didSet {
if self.count != oldValue && self.count > 0 { if self.content != oldValue {
self.button.title = self.strings.Premium_Gift_ContactSelection_Proceed self.button.title = content.title
self.button.badge = "\(self.count)" self.button.badge = content.badge.isEmpty ? nil : content.badge
if let (width, sideInset, bottomInset) = self.validLayout { if let (width, sideInset, bottomInset) = self.validLayout {
let _ = self.updateLayout(width: width, sideInset: sideInset, bottomInset: bottomInset, transition: .immediate) let _ = self.updateLayout(width: width, sideInset: sideInset, bottomInset: bottomInset, transition: .immediate)
@ -538,6 +581,8 @@ private final class FooterPanelNode: ASDisplayNode {
self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor
self.button = SolidRoundedButtonView(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 10.0) self.button = SolidRoundedButtonView(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 10.0)
self.content = Content(title: self.strings.Premium_Gift_ContactSelection_Proceed, badge: "")
super.init() super.init()

View File

@ -80,6 +80,7 @@ import ShareController
import AccountFreezeInfoScreen import AccountFreezeInfoScreen
import JoinSubjectScreen import JoinSubjectScreen
import OldChannelsController import OldChannelsController
import InviteLinksUI
private final class AccountUserInterfaceInUseContext { private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>() let subscribers = Bag<(Bool) -> Void>()
@ -1904,6 +1905,200 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return controller return controller
} }
public func openCreateGroupCallUI(context: AccountContext, peerIds: [EnginePeer.Id], parentController: ViewController) {
let _ = (context.engine.data.get(
EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
)
|> deliverOnMainQueue).startStandalone(next: { [weak parentController] peers in
guard let parentController else {
return
}
let peers = peers.compactMap({ $0 })
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(
context: context,
title: presentationData.strings.Calls_NewCall,
mode: .groupCreation(isCall: true),
options: .single([]),
filters: [.excludeSelf],
onlyWriteable: true,
isGroupInvitation: false,
isPeerEnabled: nil,
attemptDisabledItemSelection: nil,
alwaysEnabled: false,
limit: nil,
reachedLimit: nil,
openProfile: nil,
sendMessage: nil,
initialSelectedPeers: peers
))
controller.navigationPresentation = .modal
if let navigationController = parentController.navigationController as? NavigationController {
navigationController.pushViewController(controller)
} else if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(controller)
}
let _ = (controller.result
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak controller] result in
guard case let .result(rawPeerIds, _) = result else {
controller?.dismiss()
return
}
let peerIds = rawPeerIds.compactMap { id -> EnginePeer.Id? in
if case let .peer(id) = id {
return id
}
return nil
}
if peerIds.isEmpty {
controller?.dismiss()
return
}
if peerIds.count == 1 {
//TODO:release isVideo
controller?.dismiss()
self.performCall(context: context, parentController: parentController, peerId: peerIds[0], isVideo: false, began: {
let _ = (context.sharedContext.hasOngoingCall.get()
|> filter { $0 }
|> timeout(1.0, queue: Queue.mainQueue(), alternate: .single(true))
|> delay(0.5, queue: Queue.mainQueue())
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { _ in
if let controller, let navigationController = controller.navigationController as? NavigationController {
if navigationController.viewControllers.last === controller {
let _ = navigationController.popViewController(animated: true)
}
}
})
})
} else {
self.createGroupCall(context: context, parentController: parentController, peerIds: peerIds, completion: {
controller?.dismiss()
})
}
})
})
}
private func performCall(context: AccountContext, parentController: ViewController, peerId: EnginePeer.Id, isVideo: Bool, began: (() -> Void)? = nil) {
let _ = (context.account.viewTracker.peerView(peerId)
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak parentController] view in
guard let parentController else {
return
}
guard let peer = peerViewMainPeer(view) else {
return
}
if let cachedUserData = view.cachedData as? CachedUserData, cachedUserData.callsPrivate {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
parentController.present(textAlertController(context: context, title: presentationData.strings.Call_ConnectionErrorTitle, text: presentationData.strings.Call_PrivacyErrorMessage(EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
context.requestCall(peerId: peerId, isVideo: isVideo, completion: {
began?()
})
})
}
private func createGroupCall(context: AccountContext, parentController: ViewController, peerIds: [EnginePeer.Id], completion: (() -> Void)? = nil) {
parentController.view.endEditing(true)
var cancelImpl: (() -> Void)?
var signal = context.engine.calls.createConferenceCall()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { [weak parentController] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
parentController?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.3, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let disposable = (signal
|> deliverOnMainQueue).startStandalone(next: { [weak parentController] call in
guard let parentController else {
return
}
let openCall: () -> Void = {
context.sharedContext.callManager?.joinConferenceCall(
accountContext: context,
initialCall: EngineGroupCallDescription(
id: call.callInfo.id,
accessHash: call.callInfo.accessHash,
title: call.callInfo.title,
scheduleTimestamp: nil,
subscribedToScheduled: false,
isStream: false
),
reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash),
beginWithVideo: false,
invitePeerIds: peerIds
)
completion?()
}
if !peerIds.isEmpty {
openCall()
} else {
let controller = InviteLinkInviteController(
context: context,
updatedPresentationData: nil,
mode: .groupCall(InviteLinkInviteController.Mode.GroupCall(callId: call.callInfo.id, accessHash: call.callInfo.accessHash, isRecentlyCreated: true, canRevoke: true)),
initialInvite: .link(link: call.link, title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: context.account.peerId, date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil),
parentNavigationController: parentController.navigationController as? NavigationController,
completed: { [weak parentController] result in
guard let parentController else {
return
}
if let result {
switch result {
case .linkCopied:
//TODO:localize
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
parentController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: "Call link copied.", customUndoText: "View Call", timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
if case .undo = action {
openCall()
}
return false
}), in: .window(.root))
case .openCall:
openCall()
}
}
}
)
parentController.present(controller, in: .window(.root), with: nil)
}
})
cancelImpl = {
disposable.dispose()
}
}
public func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) { public func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) {
openExternalUrlImpl(context: context, urlContext: urlContext, url: url, forceExternal: forceExternal, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput) openExternalUrlImpl(context: context, urlContext: urlContext, url: url, forceExternal: forceExternal, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput)
} }