Update conference calls

This commit is contained in:
Isaac 2025-04-01 13:56:08 +04:00
parent c3c092967f
commit 54c5314462
30 changed files with 427 additions and 114 deletions

View File

@ -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<PresentationData, NoError>)? = 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<Bool, NoError> = { _ in .single(true) }, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil) {
public init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = 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<Bool, NoError> = { _ in .single(true) },
openProfile: ((EnginePeer) -> Void)? = nil,
sendMessage: ((EnginePeer) -> Void)? = nil
) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.mode = mode

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Api.Updates>) {
static func inviteConferenceCallParticipant(flags: Int32, call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
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() {

View File

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

View File

@ -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<CallSession, NoError> = .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<CallSession, NoError> = .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)
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Never, NoError> {
func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64, accessHash: Int64, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<Never, NoError> {
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<Api.Updates?, NoError> in
return .single(nil)

View File

@ -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<Never, NoError> {
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<Never, NoError> {
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<RemoveGroupCallBlockchainParticipantsResult, NoError> {

View File

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

View File

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

View File

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

View File

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

View File

@ -2670,7 +2670,7 @@ encryptDecrypt:(NSData * _Nullable (^ _Nullable)(NSData * _Nonnull, bool))encryp
},
.createWrappedAudioDeviceModule = [audioDeviceModule, isActiveByDefault](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr<tgcalls::WrappedAudioDeviceModule> {
if (audioDeviceModule) {
auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(isActiveByDefault);
auto result = audioDeviceModule->getSyncAssumingSameThread()->makeChildAudioDeviceModule(isActiveByDefault || true);
return result;
} else {
return nullptr;