mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Update conference calls
This commit is contained in:
parent
c3c092967f
commit
54c5314462
@ -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
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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?
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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) }
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) }) ?? []
|
||||
))
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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> {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user