Conference calls

This commit is contained in:
Isaac 2025-04-08 16:35:41 +04:00
parent cb83bc1b67
commit 2ab4af656b
9 changed files with 267 additions and 23 deletions

View File

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

View File

@ -109,6 +109,7 @@ public final class ContactMultiselectionControllerParams {
public let reachedLimit: ((Int32) -> Void)?
public let openProfile: ((EnginePeer) -> Void)?
public let sendMessage: ((EnginePeer) -> Void)?
public let initialSelectedPeers: [EnginePeer]
public init(
context: AccountContext,
@ -125,7 +126,8 @@ public final class ContactMultiselectionControllerParams {
limit: Int32? = nil,
reachedLimit: ((Int32) -> Void)? = nil,
openProfile: ((EnginePeer) -> Void)? = nil,
sendMessage: ((EnginePeer) -> Void)? = nil
sendMessage: ((EnginePeer) -> Void)? = nil,
initialSelectedPeers: [EnginePeer] = []
) {
self.context = context
self.updatedPresentationData = updatedPresentationData
@ -142,6 +144,7 @@ public final class ContactMultiselectionControllerParams {
self.reachedLimit = reachedLimit
self.openProfile = openProfile
self.sendMessage = sendMessage
self.initialSelectedPeers = initialSelectedPeers
}
}

View File

@ -667,8 +667,8 @@ public final class CallListController: TelegramBaseController {
return
}
self.peerViewDisposable.set((self.context.account.viewTracker.peerView(peerId)
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] view in
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] view in
if let strongSelf = self {
guard let peer = peerViewMainPeer(view) else {
return
@ -676,7 +676,6 @@ public final class CallListController: TelegramBaseController {
if let cachedUserData = view.cachedData as? CachedUserData, cachedUserData.callsPrivate {
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))
return
}
@ -700,6 +699,7 @@ public final class CallListController: TelegramBaseController {
return
}
if conferenceCall.duration != nil {
self.context.sharedContext.openCreateGroupCallUI(context: self.context, peerIds: conferenceCall.otherParticipants, parentController: self)
return
}
@ -728,6 +728,18 @@ public final class CallListController: TelegramBaseController {
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

@ -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))
|> mapError { error -> JoinLinkInfoError in
|> mapError { error -> JoinCallLinkInfoError in
switch error {
case .generic:
return .generic
}
}
|> mapToSignal { call -> Signal<JoinCallLinkInformation, JoinLinkInfoError> in
guard let call = call else {
return .fail(.generic)
|> mapToSignal { call -> Signal<JoinCallLinkInformation, JoinCallLinkInfoError> in
guard let call else {
return .fail(.doesNotExist)
}
var members: [EnginePeer] = []
for participant in call.topParticipants {

View File

@ -842,7 +842,7 @@ public extension TelegramEngine {
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)
}

View File

@ -114,7 +114,6 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
var callDuration: Int32?
var callSuccessful = true
var isVideo = false
var hasCallButton = true
var updateConferenceTimerEndTimeout: Int32?
for media in item.message.media {
if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action {
@ -173,9 +172,7 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
#else
missedTimeout = 30
#endif
if conferenceCall.duration != nil {
hasCallButton = false
}
let currentTime = Int32(Date().timeIntervalSince1970)
if conferenceCall.flags.contains(.isMissed) {
titleString = "Declined Group Call"
@ -296,9 +293,7 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
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, { [weak self] animation, _, _ in
@ -359,7 +354,6 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
if let buttonImage = buttonImage {
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.isHidden = !hasCallButton
}
if let activeConferenceUpdateTimer = strongSelf.activeConferenceUpdateTimer {
@ -411,10 +405,6 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
}
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) {
return ChatMessageBubbleContentTapAction(content: .ignore)
} else if self.bounds.contains(point), let item = self.item {

View File

@ -2894,6 +2894,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
if conferenceCall.duration != nil {
self.context.sharedContext.openCreateGroupCallUI(context: self.context, peerIds: conferenceCall.otherParticipants, parentController: self)
return
}
@ -2922,6 +2923,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
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
if let self {

View File

@ -337,7 +337,12 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
var addedToken: EditableTokenListToken?
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 selectionState: ContactListNodeGroupSelectionState?
@ -429,6 +434,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
guard let self, case let .peer(peer, _, _) = peer, let node = node as? ContextReferenceContentNode else {
return

View File

@ -80,6 +80,7 @@ import ShareController
import AccountFreezeInfoScreen
import JoinSubjectScreen
import OldChannelsController
import InviteLinksUI
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -1904,6 +1905,200 @@ public final class SharedAccountContextImpl: SharedAccountContext {
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) {
openExternalUrlImpl(context: context, urlContext: urlContext, url: url, forceExternal: forceExternal, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput)
}