mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
[WIP] Voice chats
This commit is contained in:
parent
c14d93229c
commit
c86ceafb5c
@ -198,6 +198,7 @@ public protocol PresentationGroupCall: class {
|
|||||||
var canBeRemoved: Signal<Bool, NoError> { get }
|
var canBeRemoved: Signal<Bool, NoError> { get }
|
||||||
var state: Signal<PresentationGroupCallState, NoError> { get }
|
var state: Signal<PresentationGroupCallState, NoError> { get }
|
||||||
var members: Signal<[PeerId: PresentationGroupCallMemberState], NoError> { get }
|
var members: Signal<[PeerId: PresentationGroupCallMemberState], NoError> { get }
|
||||||
|
var audioLevels: Signal<[(PeerId, Float)], NoError> { get }
|
||||||
|
|
||||||
func leave() -> Signal<Bool, NoError>
|
func leave() -> Signal<Bool, NoError>
|
||||||
|
|
||||||
|
|||||||
17
submodules/AudioBlob/BUILD
Normal file
17
submodules/AudioBlob/BUILD
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "AudioBlob",
|
||||||
|
module_name = "AudioBlob",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
deps = [
|
||||||
|
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||||
|
"//submodules/Display:Display",
|
||||||
|
"//submodules/LegacyComponents:LegacyComponents",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
||||||
@ -3,8 +3,7 @@ import UIKit
|
|||||||
import Display
|
import Display
|
||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
|
|
||||||
final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration {
|
public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration {
|
||||||
|
|
||||||
private let smallBlob: BlobView
|
private let smallBlob: BlobView
|
||||||
private let mediumBlob: BlobView
|
private let mediumBlob: BlobView
|
||||||
private let bigBlob: BlobView
|
private let bigBlob: BlobView
|
||||||
@ -18,9 +17,9 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration
|
|||||||
|
|
||||||
private(set) var isAnimating = false
|
private(set) var isAnimating = false
|
||||||
|
|
||||||
typealias BlobRange = (min: CGFloat, max: CGFloat)
|
public typealias BlobRange = (min: CGFloat, max: CGFloat)
|
||||||
|
|
||||||
init(
|
public init(
|
||||||
frame: CGRect,
|
frame: CGRect,
|
||||||
maxLevel: CGFloat,
|
maxLevel: CGFloat,
|
||||||
smallBlobRange: BlobRange,
|
smallBlobRange: BlobRange,
|
||||||
@ -84,13 +83,13 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setColor(_ color: UIColor) {
|
public func setColor(_ color: UIColor) {
|
||||||
smallBlob.setColor(color)
|
smallBlob.setColor(color)
|
||||||
mediumBlob.setColor(color.withAlphaComponent(0.3))
|
mediumBlob.setColor(color.withAlphaComponent(0.3))
|
||||||
bigBlob.setColor(color.withAlphaComponent(0.15))
|
bigBlob.setColor(color.withAlphaComponent(0.15))
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLevel(_ level: CGFloat) {
|
public func updateLevel(_ level: CGFloat) {
|
||||||
let normalizedLevel = min(1, max(level / maxLevel, 0))
|
let normalizedLevel = min(1, max(level / maxLevel, 0))
|
||||||
|
|
||||||
smallBlob.updateSpeedLevel(to: normalizedLevel)
|
smallBlob.updateSpeedLevel(to: normalizedLevel)
|
||||||
@ -100,7 +99,7 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration
|
|||||||
audioLevel = normalizedLevel
|
audioLevel = normalizedLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
func startAnimating() {
|
public func startAnimating() {
|
||||||
guard !isAnimating else { return }
|
guard !isAnimating else { return }
|
||||||
isAnimating = true
|
isAnimating = true
|
||||||
|
|
||||||
@ -112,7 +111,7 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration
|
|||||||
displayLinkAnimator?.isPaused = false
|
displayLinkAnimator?.isPaused = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopAnimating() {
|
public func stopAnimating() {
|
||||||
guard isAnimating else { return }
|
guard isAnimating else { return }
|
||||||
isAnimating = false
|
isAnimating = false
|
||||||
|
|
||||||
@ -138,7 +137,7 @@ final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override public func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
smallBlob.frame = bounds
|
smallBlob.frame = bounds
|
||||||
@ -22,6 +22,7 @@ swift_library(
|
|||||||
"//submodules/ContextUI:ContextUI",
|
"//submodules/ContextUI:ContextUI",
|
||||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||||
"//submodules/AccountContext:AccountContext",
|
"//submodules/AccountContext:AccountContext",
|
||||||
|
"//submodules/AudioBlob:AudioBlob",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import TelegramStringFormatting
|
|||||||
import PeerPresenceStatusManager
|
import PeerPresenceStatusManager
|
||||||
import ContextUI
|
import ContextUI
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import AudioBlob
|
||||||
|
|
||||||
private final class ShimmerEffectNode: ASDisplayNode {
|
private final class ShimmerEffectNode: ASDisplayNode {
|
||||||
private var currentBackgroundColor: UIColor?
|
private var currentBackgroundColor: UIColor?
|
||||||
@ -346,8 +347,9 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
|
|||||||
let shimmering: ItemListPeerItemShimmering?
|
let shimmering: ItemListPeerItemShimmering?
|
||||||
let displayDecorations: Bool
|
let displayDecorations: Bool
|
||||||
let disableInteractiveTransitionIfNecessary: Bool
|
let disableInteractiveTransitionIfNecessary: Bool
|
||||||
|
let audioLevel: Signal<Float, NoError>?
|
||||||
|
|
||||||
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false) {
|
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false, audioLevel: Signal<Float, NoError>? = nil) {
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.dateTimeFormat = dateTimeFormat
|
self.dateTimeFormat = dateTimeFormat
|
||||||
self.nameDisplayOrder = nameDisplayOrder
|
self.nameDisplayOrder = nameDisplayOrder
|
||||||
@ -380,6 +382,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
|
|||||||
self.shimmering = shimmering
|
self.shimmering = shimmering
|
||||||
self.displayDecorations = displayDecorations
|
self.displayDecorations = displayDecorations
|
||||||
self.disableInteractiveTransitionIfNecessary = disableInteractiveTransitionIfNecessary
|
self.disableInteractiveTransitionIfNecessary = disableInteractiveTransitionIfNecessary
|
||||||
|
self.audioLevel = audioLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||||
@ -452,6 +455,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||||||
|
|
||||||
private let containerNode: ContextControllerSourceNode
|
private let containerNode: ContextControllerSourceNode
|
||||||
|
|
||||||
|
private var audioLevelView: VoiceBlobView?
|
||||||
fileprivate let avatarNode: AvatarNode
|
fileprivate let avatarNode: AvatarNode
|
||||||
private let titleNode: TextNode
|
private let titleNode: TextNode
|
||||||
private let labelNode: TextNode
|
private let labelNode: TextNode
|
||||||
@ -470,6 +474,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||||||
private var editableControlNode: ItemListEditableControlNode?
|
private var editableControlNode: ItemListEditableControlNode?
|
||||||
private var reorderControlNode: ItemListEditableReorderControlNode?
|
private var reorderControlNode: ItemListEditableReorderControlNode?
|
||||||
|
|
||||||
|
private let audioLevelDisposable = MetaDisposable()
|
||||||
|
|
||||||
override public var canBeSelected: Bool {
|
override public var canBeSelected: Bool {
|
||||||
if self.editableControlNode != nil || self.disabledOverlayNode != nil {
|
if self.editableControlNode != nil || self.disabledOverlayNode != nil {
|
||||||
return false
|
return false
|
||||||
@ -499,7 +505,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||||||
self.containerNode = ContextControllerSourceNode()
|
self.containerNode = ContextControllerSourceNode()
|
||||||
|
|
||||||
self.avatarNode = AvatarNode(font: avatarFont)
|
self.avatarNode = AvatarNode(font: avatarFont)
|
||||||
self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
|
//self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
|
||||||
|
|
||||||
self.titleNode = TextNode()
|
self.titleNode = TextNode()
|
||||||
self.titleNode.isUserInteractionEnabled = false
|
self.titleNode.isUserInteractionEnabled = false
|
||||||
@ -550,6 +556,10 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.audioLevelDisposable.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
override public func didLoad() {
|
override public func didLoad() {
|
||||||
super.didLoad()
|
super.didLoad()
|
||||||
|
|
||||||
@ -1103,7 +1113,53 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||||||
|
|
||||||
strongSelf.labelBadgeNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth, y: labelFrame.minY - 1.0), size: CGSize(width: badgeWidth, height: badgeDiameter))
|
strongSelf.labelBadgeNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth, y: labelFrame.minY - 1.0), size: CGSize(width: badgeWidth, height: badgeDiameter))
|
||||||
|
|
||||||
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)))
|
let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||||
|
|
||||||
|
let blobFrame = avatarFrame.insetBy(dx: -12.0, dy: -12.0)
|
||||||
|
|
||||||
|
if let audioLevel = item.audioLevel {
|
||||||
|
strongSelf.audioLevelView?.frame = blobFrame
|
||||||
|
strongSelf.audioLevelDisposable.set((audioLevel
|
||||||
|
|> deliverOnMainQueue).start(next: { value in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strongSelf.audioLevelView == nil {
|
||||||
|
let audioLevelView = VoiceBlobView(
|
||||||
|
frame: blobFrame,
|
||||||
|
maxLevel: 0.3,
|
||||||
|
smallBlobRange: (0, 0),
|
||||||
|
mediumBlobRange: (0.7, 0.8),
|
||||||
|
bigBlobRange: (0.8, 0.9)
|
||||||
|
)
|
||||||
|
|
||||||
|
let maskRect = CGRect(origin: .zero, size: blobFrame.size)
|
||||||
|
let playbackMaskLayer = CAShapeLayer()
|
||||||
|
playbackMaskLayer.frame = maskRect
|
||||||
|
playbackMaskLayer.fillRule = .evenOdd
|
||||||
|
let maskPath = UIBezierPath()
|
||||||
|
maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 12, dy: 12), cornerRadius: 22))
|
||||||
|
maskPath.append(UIBezierPath(rect: maskRect))
|
||||||
|
playbackMaskLayer.path = maskPath.cgPath
|
||||||
|
audioLevelView.layer.mask = playbackMaskLayer
|
||||||
|
|
||||||
|
audioLevelView.setColor(.green)
|
||||||
|
strongSelf.audioLevelView = audioLevelView
|
||||||
|
strongSelf.containerNode.view.insertSubview(audioLevelView, at: 0)
|
||||||
|
audioLevelView.startAnimating()
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0)
|
||||||
|
}))
|
||||||
|
} else if let audioLevelView = strongSelf.audioLevelView {
|
||||||
|
strongSelf.audioLevelView = nil
|
||||||
|
audioLevelView.removeFromSuperview()
|
||||||
|
|
||||||
|
strongSelf.audioLevelDisposable.set(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame)
|
||||||
|
|
||||||
if item.peer.id == item.context.account.peerId, case .threatSelfAsSaved = item.aliasHandling {
|
if item.peer.id == item.context.account.peerId, case .threatSelfAsSaved = item.aliasHandling {
|
||||||
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: .savedMessagesIcon, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad)
|
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: .savedMessagesIcon, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad)
|
||||||
|
|||||||
@ -179,10 +179,10 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let fetchedData = fetchedData {
|
if let fetchedData = fetchedData {
|
||||||
precondition(fetchedData.count <= readCount)
|
assert(fetchedData.count <= readCount)
|
||||||
fetchedData.withUnsafeBytes { bytes -> Void in
|
fetchedData.withUnsafeBytes { bytes -> Void in
|
||||||
precondition(bytes.baseAddress != nil)
|
precondition(bytes.baseAddress != nil)
|
||||||
memcpy(buffer, bytes.baseAddress, fetchedData.count)
|
memcpy(buffer, bytes.baseAddress, min(fetchedData.count, readCount))
|
||||||
}
|
}
|
||||||
fetchedCount = Int32(fetchedData.count)
|
fetchedCount = Int32(fetchedData.count)
|
||||||
context.readingOffset += Int(fetchedCount)
|
context.readingOffset += Int(fetchedCount)
|
||||||
|
|||||||
@ -787,6 +787,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[-202219658] = { return Api.MessageAction.parse_messageActionContactSignUp($0) }
|
dict[-202219658] = { return Api.MessageAction.parse_messageActionContactSignUp($0) }
|
||||||
dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) }
|
dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) }
|
||||||
dict[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($0) }
|
dict[2047704898] = { return Api.MessageAction.parse_messageActionGroupCall($0) }
|
||||||
|
dict[254144570] = { return Api.MessageAction.parse_messageActionInviteToGroupCall($0) }
|
||||||
dict[1399245077] = { return Api.PhoneCall.parse_phoneCallEmpty($0) }
|
dict[1399245077] = { return Api.PhoneCall.parse_phoneCallEmpty($0) }
|
||||||
dict[462375633] = { return Api.PhoneCall.parse_phoneCallWaiting($0) }
|
dict[462375633] = { return Api.PhoneCall.parse_phoneCallWaiting($0) }
|
||||||
dict[-2014659757] = { return Api.PhoneCall.parse_phoneCallRequested($0) }
|
dict[-2014659757] = { return Api.PhoneCall.parse_phoneCallRequested($0) }
|
||||||
@ -880,7 +881,7 @@ public struct Api {
|
|||||||
return parser(reader)
|
return parser(reader)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found")
|
telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21479,6 +21479,7 @@ public extension Api {
|
|||||||
case messageActionContactSignUp
|
case messageActionContactSignUp
|
||||||
case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32)
|
case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32)
|
||||||
case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?)
|
case messageActionGroupCall(flags: Int32, call: Api.InputGroupCall, duration: Int32?)
|
||||||
|
case messageActionInviteToGroupCall(call: Api.InputGroupCall, userId: Int32)
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
switch self {
|
switch self {
|
||||||
@ -21666,6 +21667,13 @@ public extension Api {
|
|||||||
call.serialize(buffer, true)
|
call.serialize(buffer, true)
|
||||||
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(duration!, buffer: buffer, boxed: false)}
|
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(duration!, buffer: buffer, boxed: false)}
|
||||||
break
|
break
|
||||||
|
case .messageActionInviteToGroupCall(let call, let userId):
|
||||||
|
if boxed {
|
||||||
|
buffer.appendInt32(254144570)
|
||||||
|
}
|
||||||
|
call.serialize(buffer, true)
|
||||||
|
serializeInt32(userId, buffer: buffer, boxed: false)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21721,6 +21729,8 @@ public extension Api {
|
|||||||
return ("messageActionGeoProximityReached", [("fromId", fromId), ("toId", toId), ("distance", distance)])
|
return ("messageActionGeoProximityReached", [("fromId", fromId), ("toId", toId), ("distance", distance)])
|
||||||
case .messageActionGroupCall(let flags, let call, let duration):
|
case .messageActionGroupCall(let flags, let call, let duration):
|
||||||
return ("messageActionGroupCall", [("flags", flags), ("call", call), ("duration", duration)])
|
return ("messageActionGroupCall", [("flags", flags), ("call", call), ("duration", duration)])
|
||||||
|
case .messageActionInviteToGroupCall(let call, let userId):
|
||||||
|
return ("messageActionInviteToGroupCall", [("call", call), ("userId", userId)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22029,6 +22039,22 @@ public extension Api {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public static func parse_messageActionInviteToGroupCall(_ reader: BufferReader) -> MessageAction? {
|
||||||
|
var _1: Api.InputGroupCall?
|
||||||
|
if let signature = reader.readInt32() {
|
||||||
|
_1 = Api.parse(reader, signature: signature) as? Api.InputGroupCall
|
||||||
|
}
|
||||||
|
var _2: Int32?
|
||||||
|
_2 = reader.readInt32()
|
||||||
|
let _c1 = _1 != nil
|
||||||
|
let _c2 = _2 != nil
|
||||||
|
if _c1 && _c2 {
|
||||||
|
return Api.MessageAction.messageActionInviteToGroupCall(call: _1!, userId: _2!)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
public enum PhoneCall: TypeConstructorDescription {
|
public enum PhoneCall: TypeConstructorDescription {
|
||||||
|
|||||||
@ -7310,6 +7310,21 @@ public extension Api {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func inviteToGroupCall(call: Api.InputGroupCall, userId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||||
|
let buffer = Buffer()
|
||||||
|
buffer.appendInt32(-580284540)
|
||||||
|
call.serialize(buffer, true)
|
||||||
|
userId.serialize(buffer, true)
|
||||||
|
return (FunctionDescription(name: "phone.inviteToGroupCall", parameters: [("call", call), ("userId", userId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||||
|
let reader = BufferReader(buffer)
|
||||||
|
var result: Api.Updates?
|
||||||
|
if let signature = reader.readInt32() {
|
||||||
|
result = Api.parse(reader, signature: signature) as? Api.Updates
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
public static func discardGroupCall(call: Api.InputGroupCall) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
public static func discardGroupCall(call: Api.InputGroupCall) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||||
let buffer = Buffer()
|
let buffer = Buffer()
|
||||||
buffer.appendInt32(2054648117)
|
buffer.appendInt32(2054648117)
|
||||||
|
|||||||
@ -27,7 +27,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
private enum InternalState {
|
private enum InternalState {
|
||||||
case requesting
|
case requesting
|
||||||
case active(GroupCallInfo)
|
case active(GroupCallInfo)
|
||||||
case estabilished(GroupCallInfo, String, [UInt32], [UInt32: PeerId])
|
case estabilished(GroupCallInfo, String, UInt32, [UInt32], [UInt32: PeerId])
|
||||||
|
|
||||||
var callInfo: GroupCallInfo? {
|
var callInfo: GroupCallInfo? {
|
||||||
switch self {
|
switch self {
|
||||||
@ -35,7 +35,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
return nil
|
return nil
|
||||||
case let .active(info):
|
case let .active(info):
|
||||||
return info
|
return info
|
||||||
case let .estabilished(info, _, _, _):
|
case let .estabilished(info, _, _, _, _):
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,6 +75,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
return self.audioOutputStatePromise.get()
|
return self.audioOutputStatePromise.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let audioLevelsPipe = ValuePipe<[(PeerId, Float)]>()
|
||||||
|
public var audioLevels: Signal<[(PeerId, Float)], NoError> {
|
||||||
|
return self.audioLevelsPipe.signal()
|
||||||
|
}
|
||||||
|
private var audioLevelsDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var audioSessionControl: ManagedAudioSessionControl?
|
private var audioSessionControl: ManagedAudioSessionControl?
|
||||||
private var audioSessionDisposable: Disposable?
|
private var audioSessionDisposable: Disposable?
|
||||||
private let audioSessionShouldBeActive = ValuePromise<Bool>(false, ignoreRepeated: true)
|
private let audioSessionShouldBeActive = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||||
@ -120,6 +126,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
private let memberStatesDisposable = MetaDisposable()
|
private let memberStatesDisposable = MetaDisposable()
|
||||||
private let leaveDisposable = MetaDisposable()
|
private let leaveDisposable = MetaDisposable()
|
||||||
|
|
||||||
|
private var checkCallDisposable: Disposable?
|
||||||
|
private var isCurrentlyConnecting: Bool?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
accountContext: AccountContext,
|
accountContext: AccountContext,
|
||||||
audioSession: ManagedAudioSession,
|
audioSession: ManagedAudioSession,
|
||||||
@ -224,7 +233,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if case let .estabilished(callInfo, _, _, _) = strongSelf.internalState {
|
if case let .estabilished(callInfo, _, _, _, _) = strongSelf.internalState {
|
||||||
var addedSsrc: [UInt32] = []
|
var addedSsrc: [UInt32] = []
|
||||||
var removedSsrc: [UInt32] = []
|
var removedSsrc: [UInt32] = []
|
||||||
for (callId, peerId, ssrc, isAdded) in updates {
|
for (callId, peerId, ssrc, isAdded) in updates {
|
||||||
@ -259,6 +268,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
self.isMutedDisposable.dispose()
|
self.isMutedDisposable.dispose()
|
||||||
self.memberStatesDisposable.dispose()
|
self.memberStatesDisposable.dispose()
|
||||||
self.networkStateDisposable.dispose()
|
self.networkStateDisposable.dispose()
|
||||||
|
self.checkCallDisposable?.dispose()
|
||||||
|
self.audioLevelsDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateSessionState(internalState: InternalState, audioSessionControl: ManagedAudioSessionControl?) {
|
private func updateSessionState(internalState: InternalState, audioSessionControl: ManagedAudioSessionControl?) {
|
||||||
@ -275,6 +286,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
self.audioSessionShouldBeActive.set(true)
|
self.audioSessionShouldBeActive.set(true)
|
||||||
|
|
||||||
|
switch previousInternalState {
|
||||||
|
case .requesting:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if case .requesting = internalState {
|
||||||
|
self.isCurrentlyConnecting = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch previousInternalState {
|
switch previousInternalState {
|
||||||
case .active:
|
case .active:
|
||||||
break
|
break
|
||||||
@ -284,7 +304,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
self.callContext = callContext
|
self.callContext = callContext
|
||||||
self.requestDisposable.set((callContext.joinPayload
|
self.requestDisposable.set((callContext.joinPayload
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] joinPayload in
|
|> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -299,7 +319,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let clientParams = joinCallResult.callInfo.clientParams {
|
if let clientParams = joinCallResult.callInfo.clientParams {
|
||||||
strongSelf.updateSessionState(internalState: .estabilished(joinCallResult.callInfo, clientParams, joinCallResult.ssrcs, joinCallResult.ssrcMapping), audioSessionControl: strongSelf.audioSessionControl)
|
strongSelf.updateSessionState(internalState: .estabilished(joinCallResult.callInfo, clientParams, ssrc, joinCallResult.ssrcs, joinCallResult.ssrcMapping), audioSessionControl: strongSelf.audioSessionControl)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
@ -324,7 +344,21 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
case .connected:
|
case .connected:
|
||||||
mappedState = .connected
|
mappedState = .connected
|
||||||
}
|
}
|
||||||
strongSelf.stateValue.networkState = mappedState
|
if strongSelf.stateValue.networkState != mappedState {
|
||||||
|
strongSelf.stateValue.networkState = mappedState
|
||||||
|
}
|
||||||
|
|
||||||
|
let isConnecting = mappedState == .connecting
|
||||||
|
|
||||||
|
if strongSelf.isCurrentlyConnecting != isConnecting {
|
||||||
|
strongSelf.isCurrentlyConnecting = isConnecting
|
||||||
|
if isConnecting {
|
||||||
|
strongSelf.startCheckingCallIfNeeded()
|
||||||
|
} else {
|
||||||
|
strongSelf.checkCallDisposable?.dispose()
|
||||||
|
strongSelf.checkCallDisposable = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
self.memberStatesDisposable.set((callContext.memberStates
|
self.memberStatesDisposable.set((callContext.memberStates
|
||||||
@ -343,6 +377,22 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
strongSelf.membersValue = result
|
strongSelf.membersValue = result
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
self.audioLevelsDisposable.set((callContext.audioLevels
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] levels in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result: [(PeerId, Float)] = []
|
||||||
|
for (ssrc, level) in levels {
|
||||||
|
if let peerId = strongSelf.ssrcMapping[ssrc] {
|
||||||
|
result.append((peerId, level))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !result.isEmpty {
|
||||||
|
strongSelf.audioLevelsPipe.putNext(result)
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,13 +400,47 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
case .estabilished:
|
case .estabilished:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
if case let .estabilished(_, clientParams, ssrcs, ssrcMapping) = internalState {
|
if case let .estabilished(_, clientParams, _, ssrcs, ssrcMapping) = internalState {
|
||||||
self.ssrcMapping = ssrcMapping
|
self.ssrcMapping = ssrcMapping
|
||||||
self.callContext?.setJoinResponse(payload: clientParams, ssrcs: ssrcs)
|
self.callContext?.setJoinResponse(payload: clientParams, ssrcs: ssrcs)
|
||||||
|
if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting {
|
||||||
|
self.startCheckingCallIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func startCheckingCallIfNeeded() {
|
||||||
|
if self.checkCallDisposable != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if case let .estabilished(callInfo, _, ssrc, _, _) = self.internalState {
|
||||||
|
let checkSignal = checkGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, ssrc: Int32(bitPattern: ssrc))
|
||||||
|
|
||||||
|
self.checkCallDisposable = ((
|
||||||
|
checkSignal
|
||||||
|
|> castError(Bool.self)
|
||||||
|
|> delay(4.0, queue: .mainQueue())
|
||||||
|
|> mapToSignal { result -> Signal<Bool, Bool> in
|
||||||
|
if case .success = result {
|
||||||
|
return .fail(true)
|
||||||
|
} else {
|
||||||
|
return .single(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> restartIfError
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.checkCallDisposable = nil
|
||||||
|
strongSelf.requestCall()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func updateIsAudioSessionActive(_ value: Bool) {
|
private func updateIsAudioSessionActive(_ value: Bool) {
|
||||||
if self.isAudioSessionActive != value {
|
if self.isAudioSessionActive != value {
|
||||||
self.isAudioSessionActive = value
|
self.isAudioSessionActive = value
|
||||||
@ -364,7 +448,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func leave() -> Signal<Bool, NoError> {
|
public func leave() -> Signal<Bool, NoError> {
|
||||||
if case let .estabilished(callInfo, _, _, _) = self.internalState {
|
if case let .estabilished(callInfo, _, _, _, _) = self.internalState {
|
||||||
self.leaveDisposable.set((leaveGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash)
|
self.leaveDisposable.set((leaveGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash)
|
||||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||||
self?._canBeRemoved.set(.single(true))
|
self?._canBeRemoved.set(.single(true))
|
||||||
@ -402,7 +486,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func requestCall() {
|
private func requestCall() {
|
||||||
self.internalState = .requesting
|
self.callContext?.stop()
|
||||||
|
self.callContext = nil
|
||||||
|
|
||||||
|
self.updateSessionState(internalState: .requesting, audioSessionControl: self.audioSessionControl)
|
||||||
|
|
||||||
enum CallError {
|
enum CallError {
|
||||||
case generic
|
case generic
|
||||||
|
|||||||
@ -127,7 +127,28 @@ public final class VoiceChatController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class Interaction {
|
private final class Interaction {
|
||||||
|
private var audioLevels: [PeerId: ValuePipe<Float>] = [:]
|
||||||
|
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAudioLevel(_ peerId: PeerId) -> Signal<Float, NoError>? {
|
||||||
|
if let current = self.audioLevels[peerId] {
|
||||||
|
return current.signal()
|
||||||
|
} else {
|
||||||
|
let value = ValuePipe<Float>()
|
||||||
|
self.audioLevels[peerId] = value
|
||||||
|
return value.signal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAudioLevels(levels: [(PeerId, Float)]) {
|
||||||
|
for (peerId, level) in levels {
|
||||||
|
if let pipe = self.audioLevels[peerId] {
|
||||||
|
pipe.putNext(level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PeerEntry: Comparable, Identifiable {
|
private struct PeerEntry: Comparable, Identifiable {
|
||||||
@ -173,7 +194,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
//arguments.setItemIdWithRevealedOptions(lhs.flatMap { .peer($0) }, rhs.flatMap { .peer($0) })
|
//arguments.setItemIdWithRevealedOptions(lhs.flatMap { .peer($0) }, rhs.flatMap { .peer($0) })
|
||||||
}, removePeer: { id in
|
}, removePeer: { id in
|
||||||
//arguments.deleteIncludePeer(id)
|
//arguments.deleteIncludePeer(id)
|
||||||
}, noInsets: true)
|
}, noInsets: true, audioLevel: peer.id == context.account.peerId ? nil : interaction.getAudioLevel(peer.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,6 +246,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
private var audioOutputStateDisposable: Disposable?
|
private var audioOutputStateDisposable: Disposable?
|
||||||
private var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)?
|
private var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)?
|
||||||
|
|
||||||
|
private var audioLevelsDisposable: Disposable?
|
||||||
private var memberStatesDisposable: Disposable?
|
private var memberStatesDisposable: Disposable?
|
||||||
|
|
||||||
private var itemInteraction: Interaction?
|
private var itemInteraction: Interaction?
|
||||||
@ -347,12 +369,21 @@ public final class VoiceChatController: ViewController {
|
|||||||
|
|
||||||
self.audioOutputStateDisposable = (call.audioOutputState
|
self.audioOutputStateDisposable = (call.audioOutputState
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||||
if let strongSelf = self {
|
guard let strongSelf = self else {
|
||||||
strongSelf.audioOutputState = state
|
return
|
||||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
|
||||||
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
strongSelf.audioOutputState = state
|
||||||
|
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||||
|
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.audioLevelsDisposable = (call.audioLevels
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] levels in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.itemInteraction?.updateAudioLevels(levels: levels)
|
||||||
})
|
})
|
||||||
|
|
||||||
self.leaveNode.addTarget(self, action: #selector(self.leavePressed), forControlEvents: .touchUpInside)
|
self.leaveNode.addTarget(self, action: #selector(self.leavePressed), forControlEvents: .touchUpInside)
|
||||||
@ -370,6 +401,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.callStateDisposable?.dispose()
|
self.callStateDisposable?.dispose()
|
||||||
self.audioOutputStateDisposable?.dispose()
|
self.audioOutputStateDisposable?.dispose()
|
||||||
self.memberStatesDisposable?.dispose()
|
self.memberStatesDisposable?.dispose()
|
||||||
|
self.audioLevelsDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func leavePressed() {
|
@objc private func leavePressed() {
|
||||||
|
|||||||
@ -137,6 +137,44 @@ public func createGroupCall(account: Account, peerId: PeerId) -> Signal<GroupCal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct GetGroupCallParticipantsResult {
|
||||||
|
public var ssrcMapping: [UInt32: PeerId]
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum GetGroupCallParticipantsError {
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getGroupCallParticipants(account: Account, callId: Int64, accessHash: Int64, maxDate: Int32, limit: Int32) -> Signal<GetGroupCallParticipantsResult, GetGroupCallParticipantsError> {
|
||||||
|
return account.network.request(Api.functions.phone.getGroupParticipants(call: .inputGroupCall(id: callId, accessHash: accessHash), maxDate: maxDate, limit: limit))
|
||||||
|
|> mapError { _ -> GetGroupCallParticipantsError in
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
|> map { result -> GetGroupCallParticipantsResult in
|
||||||
|
var ssrcMapping: [UInt32: PeerId] = [:]
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case let .groupParticipants(count, participants, users):
|
||||||
|
for participant in participants {
|
||||||
|
var peerId: PeerId?
|
||||||
|
var ssrc: UInt32?
|
||||||
|
switch participant {
|
||||||
|
case let .groupCallParticipant(flags, userId, date, source):
|
||||||
|
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)
|
||||||
|
ssrc = UInt32(bitPattern: source)
|
||||||
|
}
|
||||||
|
if let peerId = peerId, let ssrc = ssrc {
|
||||||
|
ssrcMapping[ssrc] = peerId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetGroupCallParticipantsResult(
|
||||||
|
ssrcMapping: ssrcMapping
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum JoinGroupCallError {
|
public enum JoinGroupCallError {
|
||||||
case generic
|
case generic
|
||||||
}
|
}
|
||||||
@ -153,11 +191,17 @@ public func joinGroupCall(account: Account, callId: Int64, accessHash: Int64, jo
|
|||||||
return .generic
|
return .generic
|
||||||
}
|
}
|
||||||
|> mapToSignal { updates -> Signal<JoinGroupCallResult, JoinGroupCallError> in
|
|> mapToSignal { updates -> Signal<JoinGroupCallResult, JoinGroupCallError> in
|
||||||
return account.network.request(Api.functions.phone.getGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash)))
|
return combineLatest(
|
||||||
|> mapError { _ -> JoinGroupCallError in
|
account.network.request(Api.functions.phone.getGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash)))
|
||||||
return .generic
|
|> mapError { _ -> JoinGroupCallError in
|
||||||
}
|
return .generic
|
||||||
|> mapToSignal { result -> Signal<JoinGroupCallResult, JoinGroupCallError> in
|
},
|
||||||
|
getGroupCallParticipants(account: account, callId: callId, accessHash: accessHash, maxDate: 0, limit: 100)
|
||||||
|
|> mapError { _ -> JoinGroupCallError in
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> mapToSignal { result, participantsResult -> Signal<JoinGroupCallResult, JoinGroupCallError> in
|
||||||
account.stateManager.addUpdates(updates)
|
account.stateManager.addUpdates(updates)
|
||||||
|
|
||||||
var maybeParsedCall: GroupCallInfo?
|
var maybeParsedCall: GroupCallInfo?
|
||||||
@ -180,7 +224,7 @@ public func joinGroupCall(account: Account, callId: Int64, accessHash: Int64, jo
|
|||||||
guard let _ = GroupCallInfo(call) else {
|
guard let _ = GroupCallInfo(call) else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
}
|
}
|
||||||
var ssrcMapping: [UInt32: PeerId] = [:]
|
var ssrcMapping: [UInt32: PeerId] = participantsResult.ssrcMapping
|
||||||
for participant in participants {
|
for participant in participants {
|
||||||
var peerId: PeerId?
|
var peerId: PeerId?
|
||||||
var ssrc: UInt32?
|
var ssrc: UInt32?
|
||||||
@ -233,3 +277,23 @@ public func stopGroupCall(account: Account, callId: Int64, accessHash: Int64) ->
|
|||||||
return .complete()
|
return .complete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum CheckGroupCallResult {
|
||||||
|
case success
|
||||||
|
case restart
|
||||||
|
}
|
||||||
|
|
||||||
|
public func checkGroupCall(account: Account, callId: Int64, accessHash: Int64, ssrc: Int32) -> Signal<CheckGroupCallResult, NoError> {
|
||||||
|
return account.network.request(Api.functions.phone.checkGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash), source: ssrc))
|
||||||
|
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||||
|
return .single(.boolFalse)
|
||||||
|
}
|
||||||
|
|> map { result -> CheckGroupCallResult in
|
||||||
|
switch result {
|
||||||
|
case .boolTrue:
|
||||||
|
return .success
|
||||||
|
case .boolFalse:
|
||||||
|
return .restart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -188,7 +188,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall:
|
case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionInviteToGroupCall:
|
||||||
break
|
break
|
||||||
case let .messageActionChannelMigrateFrom(_, chatId):
|
case let .messageActionChannelMigrateFrom(_, chatId):
|
||||||
result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId))
|
result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId))
|
||||||
|
|||||||
@ -64,6 +64,8 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
|
|||||||
case let .inputGroupCall(id, accessHash):
|
case let .inputGroupCall(id, accessHash):
|
||||||
return TelegramMediaAction(action: .groupPhoneCall(callId: id, accessHash: accessHash, duration: duration))
|
return TelegramMediaAction(action: .groupPhoneCall(callId: id, accessHash: accessHash, duration: duration))
|
||||||
}
|
}
|
||||||
|
case .messageActionInviteToGroupCall:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -210,6 +210,7 @@ swift_library(
|
|||||||
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
|
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
|
||||||
"//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode",
|
"//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode",
|
||||||
"//submodules/AnimatedNavigationStripeNode:AnimatedNavigationStripeNode",
|
"//submodules/AnimatedNavigationStripeNode:AnimatedNavigationStripeNode",
|
||||||
|
"//submodules/AudioBlob:AudioBlob",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -5979,11 +5979,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(items), reactionItems: [], gesture: gesture)
|
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||||
strongSelf.presentInGlobalOverlay(contextController)
|
strongSelf.presentInGlobalOverlay(contextController)
|
||||||
}, joinGroupCall: { [weak self] activeCall in
|
}, joinGroupCall: { [weak self] activeCall in
|
||||||
}, editMessageMedia: { [weak self] messageId, draw in
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.controllerInteraction?.editMessageMedia(messageId, draw)
|
|
||||||
}
|
|
||||||
}, joinGroupCall: { [weak self] messageId in
|
|
||||||
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -6011,6 +6006,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, editMessageMedia: { [weak self] messageId, draw in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.controllerInteraction?.editMessageMedia(messageId, draw)
|
||||||
|
}
|
||||||
}, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get()))
|
}, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get()))
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import SemanticStatusNode
|
|||||||
import FileMediaResourceStatus
|
import FileMediaResourceStatus
|
||||||
import CheckNode
|
import CheckNode
|
||||||
import MusicAlbumArtResources
|
import MusicAlbumArtResources
|
||||||
|
import AudioBlob
|
||||||
|
|
||||||
private struct FetchControls {
|
private struct FetchControls {
|
||||||
let fetch: () -> Void
|
let fetch: () -> Void
|
||||||
|
|||||||
@ -207,7 +207,7 @@ final class ChatPanelInterfaceInteraction {
|
|||||||
scrollToTop: @escaping () -> Void,
|
scrollToTop: @escaping () -> Void,
|
||||||
viewReplies: @escaping (MessageId?, ChatReplyThreadMessage) -> Void,
|
viewReplies: @escaping (MessageId?, ChatReplyThreadMessage) -> Void,
|
||||||
activatePinnedListPreview: @escaping (ASDisplayNode, ContextGesture) -> Void,
|
activatePinnedListPreview: @escaping (ASDisplayNode, ContextGesture) -> Void,
|
||||||
joinGroupCall: @escaping (MessageId) -> Void,
|
joinGroupCall: @escaping (CachedChannelData.ActiveCall) -> Void,
|
||||||
editMessageMedia: @escaping (MessageId, Bool) -> Void,
|
editMessageMedia: @escaping (MessageId, Bool) -> Void,
|
||||||
statuses: ChatPanelInterfaceInteractionStatuses?
|
statuses: ChatPanelInterfaceInteractionStatuses?
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -132,8 +132,8 @@ final class ChatRecentActionsController: TelegramBaseController {
|
|||||||
}, scrollToTop: {
|
}, scrollToTop: {
|
||||||
}, viewReplies: { _, _ in
|
}, viewReplies: { _, _ in
|
||||||
}, activatePinnedListPreview: { _, _ in
|
}, activatePinnedListPreview: { _, _ in
|
||||||
}, editMessageMedia: { _, _ in
|
|
||||||
}, joinGroupCall: { _ in
|
}, joinGroupCall: { _ in
|
||||||
|
}, editMessageMedia: { _, _ in
|
||||||
}, statuses: nil)
|
}, statuses: nil)
|
||||||
|
|
||||||
self.navigationItem.titleView = self.titleView
|
self.navigationItem.titleView = self.titleView
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import TelegramPresentationData
|
|||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import ChatInterfaceState
|
import ChatInterfaceState
|
||||||
|
import AudioBlob
|
||||||
|
|
||||||
private let offsetThreshold: CGFloat = 10.0
|
private let offsetThreshold: CGFloat = 10.0
|
||||||
private let dismissOffsetThreshold: CGFloat = 70.0
|
private let dismissOffsetThreshold: CGFloat = 70.0
|
||||||
|
|||||||
@ -438,8 +438,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
|
|||||||
}, scrollToTop: {
|
}, scrollToTop: {
|
||||||
}, viewReplies: { _, _ in
|
}, viewReplies: { _, _ in
|
||||||
}, activatePinnedListPreview: { _, _ in
|
}, activatePinnedListPreview: { _, _ in
|
||||||
}, editMessageMedia: { _, _ in
|
|
||||||
}, joinGroupCall: { _ in
|
}, joinGroupCall: { _ in
|
||||||
|
}, editMessageMedia: { _, _ in
|
||||||
}, statuses: nil)
|
}, statuses: nil)
|
||||||
|
|
||||||
self.selectionPanel.interfaceInteraction = interfaceInteraction
|
self.selectionPanel.interfaceInteraction = interfaceInteraction
|
||||||
|
|||||||
@ -44,7 +44,7 @@ public final class OngoingGroupCallContext {
|
|||||||
var mainStreamAudioSsrc: UInt32?
|
var mainStreamAudioSsrc: UInt32?
|
||||||
var otherSsrcs: [UInt32] = []
|
var otherSsrcs: [UInt32] = []
|
||||||
|
|
||||||
let joinPayload = Promise<String>()
|
let joinPayload = Promise<(String, UInt32)>()
|
||||||
let networkState = ValuePromise<NetworkState>(.connecting, ignoreRepeated: true)
|
let networkState = ValuePromise<NetworkState>(.connecting, ignoreRepeated: true)
|
||||||
let isMuted = ValuePromise<Bool>(true, ignoreRepeated: true)
|
let isMuted = ValuePromise<Bool>(true, ignoreRepeated: true)
|
||||||
let memberStates = ValuePromise<[UInt32: MemberState]>([:], ignoreRepeated: true)
|
let memberStates = ValuePromise<[UInt32: MemberState]>([:], ignoreRepeated: true)
|
||||||
@ -105,7 +105,7 @@ public final class OngoingGroupCallContext {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.mainStreamAudioSsrc = ssrc
|
strongSelf.mainStreamAudioSsrc = ssrc
|
||||||
strongSelf.joinPayload.set(.single(payload))
|
strongSelf.joinPayload.set(.single((payload, ssrc)))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -170,6 +170,10 @@ public final class OngoingGroupCallContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
self.context.stop()
|
||||||
|
}
|
||||||
|
|
||||||
func setIsMuted(_ isMuted: Bool) {
|
func setIsMuted(_ isMuted: Bool) {
|
||||||
self.isMuted.set(isMuted)
|
self.isMuted.set(isMuted)
|
||||||
self.context.setIsMuted(isMuted)
|
self.context.setIsMuted(isMuted)
|
||||||
@ -179,7 +183,7 @@ public final class OngoingGroupCallContext {
|
|||||||
private let queue = Queue()
|
private let queue = Queue()
|
||||||
private let impl: QueueLocalObject<Impl>
|
private let impl: QueueLocalObject<Impl>
|
||||||
|
|
||||||
public var joinPayload: Signal<String, NoError> {
|
public var joinPayload: Signal<(String, UInt32), NoError> {
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
self.impl.with { impl in
|
self.impl.with { impl in
|
||||||
@ -269,4 +273,10 @@ public final class OngoingGroupCallContext {
|
|||||||
impl.removeSsrcs(ssrcs: ssrcs)
|
impl.removeSsrcs(ssrcs: ssrcs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func stop() {
|
||||||
|
self.impl.with { impl in
|
||||||
|
impl.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -160,6 +160,8 @@ typedef NS_ENUM(int32_t, GroupCallNetworkState) {
|
|||||||
|
|
||||||
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray<NSNumber *> * _Nonnull))audioLevelsUpdated;
|
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue networkStateUpdated:(void (^ _Nonnull)(GroupCallNetworkState))networkStateUpdated audioLevelsUpdated:(void (^ _Nonnull)(NSArray<NSNumber *> * _Nonnull))audioLevelsUpdated;
|
||||||
|
|
||||||
|
- (void)stop;
|
||||||
|
|
||||||
- (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion;
|
- (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion;
|
||||||
- (void)setJoinResponsePayload:(NSString * _Nonnull)payload;
|
- (void)setJoinResponsePayload:(NSString * _Nonnull)payload;
|
||||||
- (void)setSsrcs:(NSArray<NSNumber *> * _Nonnull)ssrcs;
|
- (void)setSsrcs:(NSArray<NSNumber *> * _Nonnull)ssrcs;
|
||||||
|
|||||||
@ -826,18 +826,26 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
|
|||||||
networkStateUpdated(isConnected ? GroupCallNetworkStateConnected : GroupCallNetworkStateConnecting);
|
networkStateUpdated(isConnected ? GroupCallNetworkStateConnected : GroupCallNetworkStateConnecting);
|
||||||
}];
|
}];
|
||||||
},
|
},
|
||||||
.audioLevelsUpdated = [weakSelf, queue, audioLevelsUpdated](std::vector<std::pair<uint32_t, float>> const &levels) {
|
.audioLevelsUpdated = [audioLevelsUpdated](std::vector<std::pair<uint32_t, float>> const &levels) {
|
||||||
NSMutableArray *result = [[NSMutableArray alloc] init];
|
NSMutableArray *result = [[NSMutableArray alloc] init];
|
||||||
for (auto &it : levels) {
|
for (auto &it : levels) {
|
||||||
[result addObject:@(it.first)];
|
[result addObject:@(it.first)];
|
||||||
[result addObject:@(it.second)];
|
[result addObject:@(it.second)];
|
||||||
}
|
}
|
||||||
|
audioLevelsUpdated(result);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)stop {
|
||||||
|
if (_instance) {
|
||||||
|
_instance->stop();
|
||||||
|
_instance.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion {
|
- (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion {
|
||||||
if (_instance) {
|
if (_instance) {
|
||||||
_instance->emitJoinPayload([completion](tgcalls::GroupJoinPayload payload) {
|
_instance->emitJoinPayload([completion](tgcalls::GroupJoinPayload payload) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user