From 54c53144621f5df822656ce56a6c85f1411512b2 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 1 Apr 2025 13:56:08 +0400 Subject: [PATCH] Update conference calls --- .../Sources/ContactSelectionController.swift | 17 ++- .../Sources/PresentationCallManager.swift | 4 +- .../AvatarNode/Sources/AvatarNode.swift | 4 + .../Sources/Node/ChatListItem.swift | 2 +- submodules/Display/Source/TextNode.swift | 2 +- .../Sources/InviteLinkInviteController.swift | 12 +- .../Sources/InviteLinkListController.swift | 1 + .../Sources/InviteLinkViewController.swift | 1 + .../ItemListPermanentInviteLinkItem.swift | 46 +++++-- .../Sources/ChannelVisibilityController.swift | 1 + .../Sources/ChannelStatsController.swift | 2 +- submodules/TelegramApi/Sources/Api0.swift | 1 + submodules/TelegramApi/Sources/Api10.swift | 26 ++++ submodules/TelegramApi/Sources/Api38.swift | 7 +- .../Sources/CallController.swift | 71 ++++++---- .../Sources/PresentationCall.swift | 122 +++++++++++++----- .../Sources/PresentationGroupCall.swift | 6 +- .../Sources/VideoChatScreen.swift | 2 +- .../VideoChatScreenInviteMembers.swift | 11 +- .../Sources/VoiceChatController.swift | 9 +- .../ApiUtils/TelegramMediaAction.swift | 22 +++- .../Sources/State/CallSessionManager.swift | 12 +- .../SyncCore_TelegramMediaAction.swift | 43 +++++- .../TelegramEngine/Calls/GroupCalls.swift | 8 +- .../Calls/TelegramEngineCalls.swift | 4 +- .../ChatMessageCallBubbleContentNode.swift | 4 +- .../Sources/TextNodeWithEntities.swift | 91 +++++++++++++ .../Sources/ApplicationContext.swift | 2 + .../TelegramUI/Sources/ChatController.swift | 6 +- .../Sources/OngoingCallThreadLocalContext.mm | 2 +- 30 files changed, 427 insertions(+), 114 deletions(-) diff --git a/submodules/AccountContext/Sources/ContactSelectionController.swift b/submodules/AccountContext/Sources/ContactSelectionController.swift index 140a078ed1..0aef7792c4 100644 --- a/submodules/AccountContext/Sources/ContactSelectionController.swift +++ b/submodules/AccountContext/Sources/ContactSelectionController.swift @@ -114,7 +114,22 @@ public final class ContactSelectionControllerParams { public let openProfile: ((EnginePeer) -> Void)? public let sendMessage: ((EnginePeer) -> Void)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: ContactSelectionControllerMode = .generic, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: Signal<[ContactListAdditionalOption], NoError> = .single([]), displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, requirePhoneNumbers: Bool = false, allowChannelsInSearch: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil) { + public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + mode: ContactSelectionControllerMode = .generic, + autoDismiss: Bool = true, + title: @escaping (PresentationStrings) -> String, + options: Signal<[ContactListAdditionalOption], NoError> = .single([]), + displayDeviceContacts: Bool = false, + displayCallIcons: Bool = false, + multipleSelection: Bool = false, + requirePhoneNumbers: Bool = false, + allowChannelsInSearch: Bool = false, + confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }, + openProfile: ((EnginePeer) -> Void)? = nil, + sendMessage: ((EnginePeer) -> Void)? = nil + ) { self.context = context self.updatedPresentationData = updatedPresentationData self.mode = mode diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 96e424ad00..e032400f5d 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -173,7 +173,7 @@ public protocol PresentationCall: AnyObject { func setCurrentAudioOutput(_ output: AudioSessionOutput) func debugInfo() -> Signal<(String, String), NoError> - func upgradeToConference(invitePeerIds: [EnginePeer.Id], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable + func upgradeToConference(invitePeers: [(id: EnginePeer.Id, isVideo: Bool)], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) } @@ -484,7 +484,7 @@ public protocol PresentationGroupCall: AnyObject { func updateTitle(_ title: String) - func invitePeer(_ peerId: EnginePeer.Id) -> Bool + func invitePeer(_ peerId: EnginePeer.Id, isVideo: Bool) -> Bool func removedPeer(_ peerId: EnginePeer.Id) var invitedPeers: Signal<[PresentationGroupCallInvitedPeer], NoError> { get } diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index a2f4621540..e03b3a67c7 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -297,15 +297,18 @@ public final class AvatarNode: ASDisplayNode { private struct Params: Equatable { let peerId: EnginePeer.Id? let resourceId: String? + let displayDimensions: CGSize let clipStyle: AvatarNodeClipStyle init( peerId: EnginePeer.Id?, resourceId: String?, + displayDimensions: CGSize, clipStyle: AvatarNodeClipStyle ) { self.peerId = peerId self.resourceId = resourceId + self.displayDimensions = displayDimensions self.clipStyle = clipStyle } } @@ -661,6 +664,7 @@ public final class AvatarNode: ASDisplayNode { let params = Params( peerId: peer?.id, resourceId: smallProfileImage?.resource.id.stringRepresentation, + displayDimensions: displayDimensions, clipStyle: clipStyle ) if self.params == params { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index e379965e2d..7205b7d450 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1748,7 +1748,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } if peer.smallProfileImage != nil && overrideImage == nil { - self.avatarNode.setPeerV2(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0)) + self.avatarNode.setPeerV2(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: avatarDiameter, height: avatarDiameter)) } else { self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForumAvatar ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0)) } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index e10fadff69..ad03f72960 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -364,7 +364,7 @@ public final class TextNodeLayout: NSObject { fileprivate let backgroundColor: UIColor? fileprivate let constrainedSize: CGSize fileprivate let explicitAlignment: NSTextAlignment - fileprivate let resolvedAlignment: NSTextAlignment + public let resolvedAlignment: NSTextAlignment fileprivate let verticalAlignment: TextVerticalAlignment fileprivate let lineSpacing: CGFloat fileprivate let cutout: TextNodeCutout? diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index a93029cab4..d1ad615edc 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -27,13 +27,15 @@ class InviteLinkInviteInteraction { let copyLink: (ExportedInvitation) -> Void let shareLink: (ExportedInvitation) -> Void let manageLinks: () -> Void + let openCallAction: () -> Void - init(context: AccountContext, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, manageLinks: @escaping () -> Void) { + init(context: AccountContext, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, manageLinks: @escaping () -> Void, openCallAction: @escaping () -> Void) { self.context = context self.mainLinkContextAction = mainLinkContextAction self.copyLink = copyLink self.shareLink = shareLink self.manageLinks = manageLinks + self.openCallAction = openCallAction } } @@ -131,6 +133,8 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable { }, contextAction: { node, gesture in interaction.mainLinkContextAction(invitation, node, gesture) }, viewAction: { + }, openCallAction: { + interaction.openCallAction() }) case let .manage(text, standalone): return InviteLinkInviteManageItem(theme: presentationData.theme, text: text, standalone: standalone, action: { @@ -546,6 +550,12 @@ public final class InviteLinkInviteController: ViewController { strongSelf.controller?.parentNavigationController?.pushViewController(controller) strongSelf.controller?.dismiss() } + }, openCallAction: { [weak self] in + guard let self else { + return + } + self.controller?.completed?(.openCall) + self.controller?.dismiss() }) let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 62acacd470..cd3dcb291a 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -229,6 +229,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { if let invite = invite { arguments.openLink(invite) } + }, openCallAction: { }) case let .mainLinkOtherInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: nil, style: .blocks, tag: nil) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index 17b5068a7b..b87a65d5e0 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -271,6 +271,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { }, contextAction: invite.link?.hasSuffix("...") == true ? nil : { node, gesture in interaction.contextAction(invite, node, gesture) }, viewAction: { + }, openCallAction: { }) case let .subscriptionHeader(_, title): return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) diff --git a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift index 6178e7cb6b..9c049ea141 100644 --- a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift @@ -14,6 +14,7 @@ import Markdown import TextFormat import ComponentFlow import MultilineTextComponent +import TextNodeWithEntities private func actionButtonImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in @@ -46,6 +47,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { let shareAction: (() -> Void)? let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? let viewAction: (() -> Void)? + let openCallAction: (() -> Void)? public let tag: ItemListItemTag? public init( @@ -65,6 +67,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { shareAction: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, viewAction: (() -> Void)?, + openCallAction: (() -> Void)?, tag: ItemListItemTag? = nil ) { self.context = context @@ -83,6 +86,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { self.shareAction = shareAction self.contextAction = contextAction self.viewAction = viewAction + self.openCallAction = openCallAction self.tag = tag } @@ -147,7 +151,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem private var shimmerNode: ShimmerEffectNode? private var absoluteLocation: (CGRect, CGSize)? - private var justCreatedCallTextNode: TextNode? + private var justCreatedCallTextNode: TextNodeWithEntities? private var justCreatedCallLeftSeparatorLayer: SimpleLayer? private var justCreatedCallRightSeparatorLayer: SimpleLayer? private var justCreatedCallSeparatorText: ComponentView? @@ -299,7 +303,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem public func asyncLayout() -> (_ item: ItemListPermanentInviteLinkItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeAddressLayout = TextNode.asyncLayout(self.addressNode) let makeInvitedPeersLayout = TextNode.asyncLayout(self.invitedPeersNode) - let makeJustCreatedCallTextNodeLayout = TextNode.asyncLayout(self.justCreatedCallTextNode) + let makeJustCreatedCallTextNodeLayout = TextNodeWithEntities.asyncLayout(self.justCreatedCallTextNode) let currentItem = self.item let avatarsContext = self.avatarsContext @@ -343,7 +347,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem let (invitedPeersLayout, invitedPeersApply) = makeInvitedPeersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: subtitle, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - var justCreatedCallTextNodeLayout: (TextNodeLayout, () -> TextNode?)? + var justCreatedCallTextNodeLayout: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities?)? if item.isCall { let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor) @@ -571,17 +575,39 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem shareButtonNode.frame = CGRect(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) if let justCreatedCallTextNodeLayout { - if let justCreatedCallTextNode = justCreatedCallTextNodeLayout.1() { + if let justCreatedCallTextNode = justCreatedCallTextNodeLayout.1(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.context.animationCache, + renderer: item.context.animationRenderer, + placeholderColor: .gray, + attemptSynchronous: true + )) { if strongSelf.justCreatedCallTextNode !== justCreatedCallTextNode { - strongSelf.justCreatedCallTextNode?.removeFromSupernode() + strongSelf.justCreatedCallTextNode?.textNode.removeFromSupernode() strongSelf.justCreatedCallTextNode = justCreatedCallTextNode - //justCreatedCallTextNode.highlig - - strongSelf.addSubnode(justCreatedCallTextNode) + strongSelf.addSubnode(justCreatedCallTextNode.textNode) } + + justCreatedCallTextNode.linkHighlightColor = item.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.1) + justCreatedCallTextNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + } + justCreatedCallTextNode.tapAttributeAction = { [weak strongSelf] attributes, _ in + guard let strongSelf else { + return + } + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + strongSelf.item?.openCallAction?() + } + } + let justCreatedCallTextNodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - justCreatedCallTextNodeLayout.0.size.width) / 2.0), y: shareButtonNode.frame.maxY + justCreatedCallTextSpacing), size: CGSize(width: justCreatedCallTextNodeLayout.0.size.width, height: justCreatedCallTextNodeLayout.0.size.height)) - justCreatedCallTextNode.frame = justCreatedCallTextNodeFrame + justCreatedCallTextNode.textNode.frame = justCreatedCallTextNodeFrame let justCreatedCallSeparatorText: ComponentView if let current = strongSelf.justCreatedCallSeparatorText { @@ -636,7 +662,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem } } else if let justCreatedCallTextNode = strongSelf.justCreatedCallTextNode { strongSelf.justCreatedCallTextNode = nil - justCreatedCallTextNode.removeFromSupernode() + justCreatedCallTextNode.textNode.removeFromSupernode() strongSelf.justCreatedCallLeftSeparatorLayer?.removeFromSuperlayer() strongSelf.justCreatedCallLeftSeparatorLayer = nil diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index dc6379afad..be840d4941 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -655,6 +655,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { if let invite = invite { arguments.openLink(invite) } + }, openCallAction: { }) case let .editablePublicLink(theme, _, placeholder, currentText): return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: currentText, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), clearType: .always, tag: ChannelVisibilityEntryTag.publicLink, sectionId: self.section, textUpdated: { updatedText in diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index bca51c83ae..98cdf8daae 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1050,7 +1050,7 @@ private enum StatsEntry: ItemListNodeEntry { arguments.copyBoostLink(link) }, shareAction: { arguments.shareBoostLink(link) - }, contextAction: nil, viewAction: nil, tag: nil) + }, contextAction: nil, viewAction: nil, openCallAction: nil, tag: nil) case let .boostersPlaceholder(_, text): return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks) case let .boostGifts(theme, title): diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 0a4b863d02..05d00fde09 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -384,6 +384,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($0) } dict[-1945083841] = { return Api.InputGroupCall.parse_inputGroupCallInviteMessage($0) } dict[-33127873] = { return Api.InputGroupCall.parse_inputGroupCallSlug($0) } + dict[-191267262] = { return Api.InputInvoice.parse_inputInvoiceBusinessBotTransferStars($0) } dict[887591921] = { return Api.InputInvoice.parse_inputInvoiceChatInviteSubscription($0) } dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) } dict[-1734841331] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftCode($0) } diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 4a461ad283..9634d92981 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -248,6 +248,7 @@ public extension Api { } public extension Api { indirect enum InputInvoice: TypeConstructorDescription { + case inputInvoiceBusinessBotTransferStars(bot: Api.InputUser, stars: Int64) case inputInvoiceChatInviteSubscription(hash: String) case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption) @@ -260,6 +261,13 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .inputInvoiceBusinessBotTransferStars(let bot, let stars): + if boxed { + buffer.appendInt32(-191267262) + } + bot.serialize(buffer, true) + serializeInt64(stars, buffer: buffer, boxed: false) + break case .inputInvoiceChatInviteSubscription(let hash): if boxed { buffer.appendInt32(887591921) @@ -329,6 +337,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .inputInvoiceBusinessBotTransferStars(let bot, let stars): + return ("inputInvoiceBusinessBotTransferStars", [("bot", bot as Any), ("stars", stars as Any)]) case .inputInvoiceChatInviteSubscription(let hash): return ("inputInvoiceChatInviteSubscription", [("hash", hash as Any)]) case .inputInvoiceMessage(let peer, let msgId): @@ -350,6 +360,22 @@ public extension Api { } } + public static func parse_inputInvoiceBusinessBotTransferStars(_ reader: BufferReader) -> InputInvoice? { + var _1: Api.InputUser? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputUser + } + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputInvoice.inputInvoiceBusinessBotTransferStars(bot: _1!, stars: _2!) + } + else { + return nil + } + } public static func parse_inputInvoiceChatInviteSubscription(_ reader: BufferReader) -> InputInvoice? { var _1: String? _1 = parseString(reader) diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 7bb1a87e73..22f67ccbb2 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -10136,12 +10136,13 @@ public extension Api.functions.phone { } } public extension Api.functions.phone { - static func inviteConferenceCallParticipant(call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func inviteConferenceCallParticipant(flags: Int32, call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1050474478) + buffer.appendInt32(-1124981115) + serializeInt32(flags, buffer: buffer, boxed: false) call.serialize(buffer, true) userId.serialize(buffer, true) - return (FunctionDescription(name: "phone.inviteConferenceCallParticipant", parameters: [("call", String(describing: call)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "phone.inviteConferenceCallParticipant", parameters: [("flags", String(describing: flags)), ("call", String(describing: call)), ("userId", String(describing: userId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 020a0d321a..57dd437ecb 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -486,45 +486,60 @@ public final class CallController: ViewController { var disablePeerIds: [EnginePeer.Id] = [] disablePeerIds.append(self.call.context.account.peerId) disablePeerIds.append(self.call.peerId) - let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in + let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, completion: { [weak self] peers in guard let self else { return } - let _ = self.call.upgradeToConference(invitePeerIds: peerIds, completion: { _ in + let _ = self.call.upgradeToConference(invitePeers: peers, completion: { _ in }) }) self.push(controller) } - static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], completion: @escaping ([EnginePeer.Id]) -> Void) -> ViewController { + static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], completion: @escaping ([(id: EnginePeer.Id, isVideo: Bool)]) -> Void) -> ViewController { //TODO:localize let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) - let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams( + + var options: [ContactListAdditionalOption] = [] + //TODO:localize + options.append(ContactListAdditionalOption(title: "Share Call Link", icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: { + //TODO:release + }, clearHighlightAutomatically: false)) + + let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( context: context, updatedPresentationData: (initial: presentationData, signal: .single(presentationData)), - title: "Invite Members", - mode: .peerSelection(searchChatList: true, searchGroups: false, searchChannels: false), - isPeerEnabled: { peer in - guard case let .user(user) = peer else { - return false + mode: .generic, + title: { strings in + //TODO:localize + return "Add Member" + }, + options: .single(options), + displayCallIcons: true, + confirmation: { peer in + switch peer { + case let .peer(peer, _, _): + let peer = EnginePeer(peer) + guard case let .user(user) = peer else { + return .single(false) + } + if disablePeerIds.contains(user.id) { + return .single(false) + } + if user.botInfo != nil { + return .single(false) + } + return .single(true) + default: + return .single(false) } - if disablePeerIds.contains(user.id) { - return false - } - if user.botInfo != nil { - return false - } - return true } )) + controller.navigationPresentation = .modal let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in - guard case let .result(peerIds, _) = result else { - controller?.dismiss() - return - } - if peerIds.isEmpty { + guard let result, let peer = result.0.first, case let .peer(peer, _, _) = peer else { controller?.dismiss() return } @@ -533,15 +548,15 @@ public final class CallController: ViewController { controller?.dismiss() } - let invitePeerIds = peerIds.compactMap { item -> EnginePeer.Id? in - if case let .peer(peerId) = item { - return peerId - } else { - return nil - } + var isVideo = false + switch result.1 { + case .videoCall: + isVideo = true + default: + break } - completion(invitePeerIds) + completion([(peer.id, isVideo)]) }) return controller diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index d00ece5286..da09ba65df 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -234,6 +234,7 @@ public final class PresentationCallImpl: PresentationCall { public let peerId: EnginePeer.Id public let isOutgoing: Bool private let incomingConferenceSource: EngineMessage.Id? + private let conferenceStableId: Int64? public var isVideo: Bool public var isVideoPossible: Bool private let enableStunMarking: Bool @@ -368,7 +369,7 @@ public final class PresentationCallImpl: PresentationCall { return self.conferenceStatePromise.get() } - public private(set) var pendingInviteToConferencePeerIds: [EnginePeer.Id] = [] + public private(set) var pendingInviteToConferencePeerIds: [(id: EnginePeer.Id, isVideo: Bool)] = [] private var localVideoEndpointId: String? private var remoteVideoEndpointId: String? @@ -423,6 +424,11 @@ public final class PresentationCallImpl: PresentationCall { self.peerId = peerId self.isOutgoing = isOutgoing self.incomingConferenceSource = incomingConferenceSource + if let _ = incomingConferenceSource { + self.conferenceStableId = Int64.random(in: Int64.min ..< Int64.max) + } else { + self.conferenceStableId = nil + } self.isVideo = initialState?.type == .video self.isVideoPossible = isVideoPossible self.enableStunMarking = enableStunMarking @@ -445,19 +451,67 @@ public final class PresentationCallImpl: PresentationCall { var didReceiveAudioOutputs = false - var callSessionState: Signal = .complete() - if let initialState = initialState { - callSessionState = .single(initialState) - } - callSessionState = callSessionState - |> then(callSessionManager.callState(internalId: internalId)) - - self.sessionStateDisposable = (callSessionState - |> deliverOnMainQueue).start(next: { [weak self] sessionState in - if let strongSelf = self { - strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: strongSelf.reception, audioSessionControl: strongSelf.audioSessionControl) + if let incomingConferenceSource = incomingConferenceSource { + self.sessionStateDisposable = (context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Messages.Message(id: incomingConferenceSource) + ) + |> deliverOnMainQueue).startStrict(next: { [weak self] message in + guard let self else { + return + } + + let state: CallSessionState + if let message = message { + var foundAction: TelegramMediaAction? + for media in message.media { + if let action = media as? TelegramMediaAction { + foundAction = action + break + } + } + + if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action { + if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil { + state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions()) + } else { + state = .ringing + } + } else { + state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions()) + } + } else { + state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions()) + } + + self.updateSessionState( + sessionState: CallSession( + id: self.internalId, + stableId: self.conferenceStableId, + isOutgoing: false, + type: self.isVideo ? .video : .audio, + state: state, + isVideoPossible: true + ), + callContextState: nil, + reception: nil, + audioSessionControl: self.audioSessionControl + ) + }) + } else { + var callSessionState: Signal = .complete() + if let initialState = initialState { + callSessionState = .single(initialState) } - }) + callSessionState = callSessionState + |> then(callSessionManager.callState(internalId: internalId)) + + self.sessionStateDisposable = (callSessionState + |> deliverOnMainQueue).start(next: { [weak self] sessionState in + if let strongSelf = self { + strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: strongSelf.reception, audioSessionControl: strongSelf.audioSessionControl) + } + }) + } if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_call_device"] { self.sharedAudioContext = nil @@ -933,15 +987,20 @@ public final class PresentationCallImpl: PresentationCall { } let keyPair: TelegramKeyPair? = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() guard let keyPair, let groupCall else { - self.updateSessionState(sessionState: CallSession( - id: self.internalId, - stableId: nil, - isOutgoing: false, - type: .audio, - state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()), - isVideoPossible: true - ), - callContextState: nil, reception: nil, audioSessionControl: self.audioSessionControl) + self.sessionStateDisposable?.dispose() + self.updateSessionState( + sessionState: CallSession( + id: self.internalId, + stableId: self.conferenceStableId, + isOutgoing: false, + type: self.isVideo ? .video : .audio, + state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()), + isVideoPossible: true + ), + callContextState: nil, + reception: nil, + audioSessionControl: self.audioSessionControl + ) return } @@ -973,8 +1032,8 @@ public final class PresentationCallImpl: PresentationCall { conferenceCall.upgradedConferenceCall = self conferenceCall.setConferenceInvitedPeers(self.pendingInviteToConferencePeerIds) - for peerId in self.pendingInviteToConferencePeerIds { - let _ = conferenceCall.invitePeer(peerId) + for (peerId, isVideo) in self.pendingInviteToConferencePeerIds { + let _ = conferenceCall.invitePeer(peerId, isVideo: isVideo) } conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted) @@ -1067,9 +1126,10 @@ public final class PresentationCallImpl: PresentationCall { guard let self else { return } + self.sessionStateDisposable?.dispose() self.updateSessionState(sessionState: CallSession( id: self.internalId, - stableId: nil, + stableId: self.conferenceStableId, isOutgoing: false, type: .audio, state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()), @@ -1341,11 +1401,12 @@ public final class PresentationCallImpl: PresentationCall { if strongSelf.incomingConferenceSource != nil { strongSelf.conferenceStateValue = .preparing strongSelf.isAcceptingIncomingConference = true + strongSelf.sessionStateDisposable?.dispose() strongSelf.updateSessionState(sessionState: CallSession( id: strongSelf.internalId, - stableId: nil, + stableId: strongSelf.conferenceStableId, isOutgoing: false, - type: .audio, + type: strongSelf.isVideo ? .video : .audio, state: .ringing, isVideoPossible: true ), @@ -1365,9 +1426,10 @@ public final class PresentationCallImpl: PresentationCall { if strongSelf.incomingConferenceSource != nil { strongSelf.conferenceStateValue = .preparing strongSelf.isAcceptingIncomingConference = true + strongSelf.sessionStateDisposable?.dispose() strongSelf.updateSessionState(sessionState: CallSession( id: strongSelf.internalId, - stableId: nil, + stableId: strongSelf.conferenceStableId, isOutgoing: false, type: .audio, state: .ringing, @@ -1552,7 +1614,7 @@ public final class PresentationCallImpl: PresentationCall { self.videoCapturer?.setIsVideoEnabled(!isPaused) } - public func upgradeToConference(invitePeerIds: [EnginePeer.Id], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable { + public func upgradeToConference(invitePeers: [(id: EnginePeer.Id, isVideo: Bool)], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable { if self.isMovedToConference { return EmptyDisposable } @@ -1561,7 +1623,7 @@ public final class PresentationCallImpl: PresentationCall { return EmptyDisposable } - self.pendingInviteToConferencePeerIds = invitePeerIds + self.pendingInviteToConferencePeerIds = invitePeers let index = self.upgradedToConferenceCompletions.add({ call in completion(call) }) diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 1b00f4fe9f..83e85c6728 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -3838,14 +3838,14 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { })) } - public func invitePeer(_ peerId: PeerId) -> Bool { + public func invitePeer(_ peerId: PeerId, isVideo: Bool) -> Bool { if self.isConference { guard let initialCall = self.initialCall else { return false } //TODO:release - let _ = self.accountContext.engine.calls.inviteConferenceCallParticipant(callId: initialCall.description.id, accessHash: initialCall.description.accessHash, peerId: peerId).start() + let _ = self.accountContext.engine.calls.inviteConferenceCallParticipant(callId: initialCall.description.id, accessHash: initialCall.description.accessHash, peerId: peerId, isVideo: isVideo).start() return false /*guard let initialCall = self.initialCall else { return false @@ -3922,7 +3922,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } - func setConferenceInvitedPeers(_ peerIds: [PeerId]) { + func setConferenceInvitedPeers(_ invitedPeers: [(id: PeerId, isVideo: Bool)]) { //TODO:release /*self.invitedPeersValue = peerIds.map { PresentationGroupCallInvitedPeer(id: $0, state: .requesting) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index d31b49c950..a28b80b040 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1051,7 +1051,7 @@ final class VideoChatScreenComponent: Component { static func groupCallStateForConferenceSource(conferenceSource: PresentationCall) -> Signal<(state: PresentationGroupCallState, invitedPeers: [InvitedPeer]), NoError> { let invitedPeers = conferenceSource.context.engine.data.subscribe( - EngineDataList((conferenceSource as! PresentationCallImpl).pendingInviteToConferencePeerIds.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0) }) + EngineDataList((conferenceSource as! PresentationCallImpl).pendingInviteToConferencePeerIds.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0.id) }) ) let accountPeerId = conferenceSource.context.account.peerId diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift index 422c2cbe5e..ecffcf4552 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift @@ -57,7 +57,7 @@ extension VideoChatScreenComponent.View { } for peerId in peerIds { - let _ = groupCall.invitePeer(peerId) + let _ = groupCall.invitePeer(peerId.id, isVideo: peerId.isVideo) } }) self.environment?.controller()?.push(controller) @@ -146,7 +146,8 @@ extension VideoChatScreenComponent.View { if let participant { dismissController?() - if groupCall.invitePeer(participant.peer.id) { + //TODO:release + if groupCall.invitePeer(participant.peer.id, isVideo: false) { let text: String if case let .channel(channel) = self.peer, case .broadcast = channel.info { text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string @@ -258,7 +259,8 @@ extension VideoChatScreenComponent.View { } dismissController?() - if groupCall.invitePeer(peer.id) { + //TODO:release + if groupCall.invitePeer(peer.id, isVideo: false) { let text: String if case let .channel(channel) = self.peer, case .broadcast = channel.info { text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string @@ -330,7 +332,8 @@ extension VideoChatScreenComponent.View { } dismissController?() - if groupCall.invitePeer(peer.id) { + //TODO:release + if groupCall.invitePeer(peer.id, isVideo: false) { let text: String if case let .channel(channel) = self.peer, case .broadcast = channel.info { text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index be28d3b3af..60cefcd87c 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -1256,7 +1256,8 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController { if let participant = participant { dismissController?() - if strongSelf.call.invitePeer(participant.peer.id) { + //TODO:release + if strongSelf.call.invitePeer(participant.peer.id, isVideo: false) { let text: String if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string @@ -1364,7 +1365,8 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController { } dismissController?() - if strongSelf.call.invitePeer(peer.id) { + //TODO:release + if strongSelf.call.invitePeer(peer.id, isVideo: false) { let text: String if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string @@ -1432,7 +1434,8 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController { } dismissController?() - if strongSelf.call.invitePeer(peer.id) { + //TODO:release + if strongSelf.call.invitePeer(peer.id, isVideo: false) { let text: String if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { text = strongSelf.presentationData.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 304915e48d..3edb3bf37b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -201,12 +201,28 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars)) case let .messageActionPaidMessagesPrice(stars): return TelegramMediaAction(action: .paidMessagesPriceEdited(stars: stars)) - case let .messageActionConferenceCall(_, callId, duration, otherParticipants): - return TelegramMediaAction(action: .conferenceCall( + case let .messageActionConferenceCall(flags, callId, duration, otherParticipants): + let isMissed = (flags & (1 << 0)) != 0 + let isActive = (flags & (1 << 1)) != 0 + let isVideo = (flags & (1 << 4)) != 0 + + var mappedFlags = TelegramMediaActionType.ConferenceCall.Flags() + if isMissed { + mappedFlags.insert(.isMissed) + } + if isActive { + mappedFlags.insert(.isActive) + } + if isVideo { + mappedFlags.insert(.isVideo) + } + + return TelegramMediaAction(action: .conferenceCall(TelegramMediaActionType.ConferenceCall( callId: callId, duration: duration, + flags: mappedFlags, otherParticipants: otherParticipants.flatMap({ return $0.map(\.peerId) }) ?? [] - )) + ))) } } diff --git a/submodules/TelegramCore/Sources/State/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift index a1dfc4658e..910244cf49 100644 --- a/submodules/TelegramCore/Sources/State/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -382,7 +382,7 @@ private final class CallSessionContext { private final class IncomingConferenceInvitationContext { enum State: Equatable { case pending - case ringing(callId: Int64, otherParticipants: [EnginePeer]) + case ringing(callId: Int64, isVideo: Bool, otherParticipants: [EnginePeer]) case stopped } @@ -420,11 +420,11 @@ private final class IncomingConferenceInvitationContext { } } - if let action = foundAction, case let .conferenceCall(callId, duration, otherParticipants) = action.action { - if duration != nil { + if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action { + if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil { state = .stopped } else { - state = .ringing(callId: callId, otherParticipants: otherParticipants.compactMap { id -> EnginePeer? in + state = .ringing(callId: conferenceCall.callId, isVideo: conferenceCall.flags.contains(.isVideo), otherParticipants: conferenceCall.otherParticipants.compactMap { id -> EnginePeer? in return message.peers[id].flatMap(EnginePeer.init) }) } @@ -639,11 +639,11 @@ private final class CallSessionManagerContext { } } for (id, context) in self.incomingConferenceInvitationContexts { - if case let .ringing(_, otherParticipants) = context.state { + if case let .ringing(_, isVideo, otherParticipants) = context.state { ringingContexts.append(CallSessionRingingState( id: context.internalId, peerId: id.peerId, - isVideo: false, + isVideo: isVideo, isVideoPossible: true, conferenceSource: id, otherParticipants: otherParticipants diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 5af6c51f2f..9800d6db6b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -86,6 +86,31 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } } + public struct ConferenceCall: Equatable { + public struct Flags: OptionSet { + public var rawValue: Int32 + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let isVideo = Flags(rawValue: 1 << 0) + public static let isActive = Flags(rawValue: 1 << 1) + public static let isMissed = Flags(rawValue: 1 << 2) + } + + public let callId: Int64 + public let duration: Int32? + public let flags: Flags + public let otherParticipants: [PeerId] + + public init(callId: Int64, duration: Int32?, flags: Flags, otherParticipants: [PeerId]) { + self.callId = callId + self.duration = duration + self.flags = flags + self.otherParticipants = otherParticipants + } + } + case unknown case groupCreated(title: String) case addedMembers(peerIds: [PeerId]) @@ -134,7 +159,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?) case paidMessagesRefunded(count: Int32, stars: Int64) case paidMessagesPriceEdited(stars: Int64) - case conferenceCall(callId: Int64, duration: Int32?, otherParticipants: [PeerId]) + case conferenceCall(ConferenceCall) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -264,7 +289,12 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 47: self = .paidMessagesPriceEdited(stars: decoder.decodeInt64ForKey("stars", orElse: 0)) case 48: - self = .conferenceCall(callId: decoder.decodeInt64ForKey("cid", orElse: 0), duration: decoder.decodeOptionalInt32ForKey("dur"), otherParticipants: decoder.decodeInt64ArrayForKey("part").map(PeerId.init)) + self = .conferenceCall(ConferenceCall( + callId: decoder.decodeInt64ForKey("cid", orElse: 0), + duration: decoder.decodeOptionalInt32ForKey("dur"), + flags: ConferenceCall.Flags(rawValue: decoder.decodeInt32ForKey("flags", orElse: 0)), + otherParticipants: decoder.decodeInt64ArrayForKey("part").map(PeerId.init) + )) default: self = .unknown } @@ -642,15 +672,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case let .paidMessagesPriceEdited(stars): encoder.encodeInt32(47, forKey: "_rawValue") encoder.encodeInt64(stars, forKey: "stars") - case let .conferenceCall(callId, duration, otherParticipants): + case let .conferenceCall(conferenceCall): encoder.encodeInt32(48, forKey: "_rawValue") - encoder.encodeInt64(callId, forKey: "cid") - if let duration { + encoder.encodeInt64(conferenceCall.callId, forKey: "cid") + if let duration = conferenceCall.duration { encoder.encodeInt32(duration, forKey: "dur") } else { encoder.encodeNil(forKey: "dur") } - encoder.encodeInt64Array(otherParticipants.map({ $0.toInt64() }), forKey: "part") + encoder.encodeInt32(conferenceCall.flags.rawValue, forKey: "flags") + encoder.encodeInt64Array(conferenceCall.otherParticipants.map({ $0.toInt64() }), forKey: "part") } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 19b50bbb48..51247eb89d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -831,7 +831,7 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?, } } -func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64, accessHash: Int64, peerId: EnginePeer.Id) -> Signal { +func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64, accessHash: Int64, peerId: EnginePeer.Id, isVideo: Bool) -> Signal { return account.postbox.transaction { transaction -> Api.InputUser? in return transaction.getPeer(peerId).flatMap(apiInputUser) } @@ -840,7 +840,11 @@ func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64, return .complete() } - return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(call: .inputGroupCall(id: callId, accessHash: accessHash), userId: inputPeer)) + var flags: Int32 = 0 + if isVideo { + flags |= 1 << 0 + } + return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(flags: flags, call: .inputGroupCall(id: callId, accessHash: accessHash), userId: inputPeer)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift index c5e06f565d..c4d2655082 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/TelegramEngineCalls.swift @@ -105,8 +105,8 @@ public extension TelegramEngine { return _internal_sendConferenceCallBroadcast(account: self.account, callId: callId, accessHash: accessHash, block: block) } - public func inviteConferenceCallParticipant(callId: Int64, accessHash: Int64, peerId: EnginePeer.Id) -> Signal { - return _internal_inviteConferenceCallParticipant(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId) + public func inviteConferenceCallParticipant(callId: Int64, accessHash: Int64, peerId: EnginePeer.Id, isVideo: Bool) -> Signal { + return _internal_inviteConferenceCallParticipant(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId, isVideo: isVideo) } public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift index 642e0657e5..a7e033ea74 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode/Sources/ChatMessageCallBubbleContentNode.swift @@ -123,9 +123,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { } } break - } else if let action = media as? TelegramMediaAction, case let .conferenceCall(_, duration, _) = action.action { + } else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action { isVideo = false - callDuration = duration + callDuration = conferenceCall.duration //TODO:localize titleString = "Group Call" break diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 76198269d7..3e392a1ea7 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -136,6 +136,20 @@ public final class TextNodeWithEntities { } } + private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? + private var linkHighlightingNode: LinkHighlightingNode? + + public var linkHighlightColor: UIColor? + public var linkHighlightInset: UIEdgeInsets = .zero + + public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)? + public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)? + public var highlightAttributeAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? { + didSet { + self.updateInteractiveActions() + } + } + public init() { self.textNode = TextNode() } @@ -301,6 +315,83 @@ public final class TextNodeWithEntities { self.inlineStickerItemLayers.removeValue(forKey: key) } } + + private func updateInteractiveActions() { + if self.highlightAttributeAction != nil { + if self.tapRecognizer == nil { + let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:))) + tapRecognizer.highlight = { [weak self] point in + if let strongSelf = self, let cachedLayout = strongSelf.textNode.cachedLayout { + var rects: [CGRect]? + if let point = point { + if let (index, attributes) = strongSelf.textNode.attributesAtPoint(CGPoint(x: point.x, y: point.y)) { + if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) { + let initialRects = strongSelf.textNode.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index) + if let initialRects = initialRects, case .center = cachedLayout.resolvedAlignment { + var mappedRects: [CGRect] = [] + for i in 0 ..< initialRects.count { + let lineRect = initialRects[i].0 + var itemRect = initialRects[i].1 + itemRect.origin.x = floor((strongSelf.textNode.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x + mappedRects.append(itemRect) + } + rects = mappedRects + } else { + rects = strongSelf.textNode.attributeRects(name: selectedAttribute.rawValue, at: index) + } + } + } + } + + if var rects, !rects.isEmpty { + let linkHighlightingNode: LinkHighlightingNode + if let current = strongSelf.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear) + strongSelf.linkHighlightingNode = linkHighlightingNode + strongSelf.textNode.addSubnode(linkHighlightingNode) + } + linkHighlightingNode.frame = strongSelf.textNode.bounds + rects[rects.count - 1] = rects[rects.count - 1].inset(by: strongSelf.linkHighlightInset) + linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) }) + } else if let linkHighlightingNode = strongSelf.linkHighlightingNode { + strongSelf.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + } + self.textNode.view.addGestureRecognizer(tapRecognizer) + } + } else if let tapRecognizer = self.tapRecognizer { + self.tapRecognizer = nil + self.textNode.view.removeGestureRecognizer(tapRecognizer) + } + } + + @objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x, y: location.y)) { + self.tapAttributeAction?(attributes, index) + } + case .longTap: + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x, y: location.y)) { + self.longTapAttributeAction?(attributes, index) + } + default: + break + } + } + default: + break + } + } } public class ImmediateTextNodeWithEntities: TextNode { diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 5aae2416fa..1f0026c3e4 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -398,6 +398,8 @@ final class AuthorizedApplicationContext { if let action = media as? TelegramMediaAction { if case .messageAutoremoveTimeoutUpdated = action.action { return + } else if case .conferenceCall = action.action { + return } } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 62897ece25..cb03079237 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2879,14 +2879,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break } } - guard case let .conferenceCall(callId, duration, _) = action?.action else { + guard case let .conferenceCall(conferenceCall) = action?.action else { return } - if duration != nil { + if conferenceCall.duration != nil { return } - if let currentGroupCallController = self.context.sharedContext as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == callId { + if let currentGroupCallController = self.context.sharedContext as? VoiceChatController, case let .group(groupCall) = currentGroupCallController.call, let currentCallId = groupCall.callId, currentCallId == conferenceCall.callId { self.context.sharedContext.navigateToCurrentCall() return } diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 2b7c910bbf..abcfdbd27c 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -2670,7 +2670,7 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp }, .createWrappedAudioDeviceModule = [audioDeviceModule, isActiveByDefault](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr { if (audioDeviceModule) { - auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(isActiveByDefault); + auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(isActiveByDefault || true); return result; } else { return nullptr;