mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Conference updates
This commit is contained in:
parent
c64c8ab240
commit
9e165ca150
@ -930,6 +930,7 @@ private final class NotificationServiceHandler {
|
||||
var id: Int64
|
||||
var fromId: PeerId
|
||||
var fromTitle: String
|
||||
var memberCount: Int
|
||||
var isVideo: Bool
|
||||
var messageId: Int32
|
||||
var accountId: Int64
|
||||
@ -972,11 +973,16 @@ private final class NotificationServiceHandler {
|
||||
if let callId = Int64(callIdString), let messageId = Int32(messageIdString) {
|
||||
if let fromTitle = payloadJson["call_conference_from"] as? String {
|
||||
let isVideo = locKey == "CONF_VIDEOCALL_REQUEST"
|
||||
var memberCount = 0
|
||||
if let callParticipantsCountString = payloadJson["call_participants_cnt"] as? String, let callParticipantsCount = Int(callParticipantsCountString) {
|
||||
memberCount = callParticipantsCount
|
||||
}
|
||||
|
||||
groupCallData = GroupCallData(
|
||||
id: callId,
|
||||
fromId: peerId,
|
||||
fromTitle: fromTitle,
|
||||
memberCount: memberCount,
|
||||
isVideo: isVideo,
|
||||
messageId: messageId,
|
||||
accountId: recordId.int64
|
||||
@ -1292,7 +1298,8 @@ private final class NotificationServiceHandler {
|
||||
var voipPayload: [AnyHashable: Any] = [
|
||||
"group_call_id": "\(groupCallData.id)",
|
||||
"msg_id": "\(groupCallData.messageId)",
|
||||
"video": "0",
|
||||
"video": "\(groupCallData.isVideo)",
|
||||
"member_count": "\(groupCallData.memberCount)",
|
||||
"from_id": "\(groupCallData.fromId.id._internalGetInt64Value())",
|
||||
"from_title": groupCallData.fromTitle,
|
||||
"accountId": "\(groupCallData.accountId)"
|
||||
|
@ -0,0 +1,192 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
|
||||
public final class AccountGroupCallContextImpl: AccountGroupCallContext {
|
||||
public final class Proxy {
|
||||
public let context: AccountGroupCallContextImpl
|
||||
let removed: () -> Void
|
||||
|
||||
public init(context: AccountGroupCallContextImpl, removed: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.removed = removed
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.removed()
|
||||
}
|
||||
|
||||
public func keep() {
|
||||
}
|
||||
}
|
||||
|
||||
var disposable: Disposable?
|
||||
public var participantsContext: GroupCallParticipantsContext?
|
||||
|
||||
private let panelDataPromise = Promise<GroupCallPanelData?>()
|
||||
public var panelData: Signal<GroupCallPanelData?, NoError> {
|
||||
return self.panelDataPromise.get()
|
||||
}
|
||||
|
||||
public init(account: Account, engine: TelegramEngine, peerId: EnginePeer.Id?, isChannel: Bool, call: EngineGroupCallDescription) {
|
||||
self.panelDataPromise.set(.single(nil))
|
||||
let state = engine.calls.getGroupCallParticipants(reference: .id(id: call.id, accessHash: call.accessHash), offset: "", ssrcs: [], limit: 100, sortAscending: nil)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
let peer: Signal<EnginePeer?, NoError>
|
||||
if let peerId {
|
||||
peer = engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
} else {
|
||||
peer = .single(nil)
|
||||
}
|
||||
self.disposable = (combineLatest(queue: .mainQueue(),
|
||||
state,
|
||||
peer
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state, peer in
|
||||
guard let self, let state = state else {
|
||||
return
|
||||
}
|
||||
let context = engine.calls.groupCall(
|
||||
peerId: peerId,
|
||||
myPeerId: account.peerId,
|
||||
id: call.id,
|
||||
reference: .id(id: call.id, accessHash: call.accessHash),
|
||||
state: state,
|
||||
previousServiceState: nil
|
||||
)
|
||||
|
||||
self.participantsContext = context
|
||||
|
||||
if let peerId {
|
||||
self.panelDataPromise.set(combineLatest(queue: .mainQueue(),
|
||||
context.state,
|
||||
context.activeSpeakers
|
||||
)
|
||||
|> map { state, activeSpeakers -> GroupCallPanelData in
|
||||
var topParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||
for participant in state.participants {
|
||||
if topParticipants.count >= 3 {
|
||||
break
|
||||
}
|
||||
topParticipants.append(participant)
|
||||
}
|
||||
|
||||
var isChannel = false
|
||||
if let peer = peer, case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||
isChannel = true
|
||||
}
|
||||
|
||||
return GroupCallPanelData(
|
||||
peerId: peerId,
|
||||
isChannel: isChannel,
|
||||
info: GroupCallInfo(
|
||||
id: call.id,
|
||||
accessHash: call.accessHash,
|
||||
participantCount: state.totalCount,
|
||||
streamDcId: nil,
|
||||
title: state.title,
|
||||
scheduleTimestamp: state.scheduleTimestamp,
|
||||
subscribedToScheduled: state.subscribedToScheduled,
|
||||
recordingStartTimestamp: nil,
|
||||
sortAscending: state.sortAscending,
|
||||
defaultParticipantsAreMuted: state.defaultParticipantsAreMuted,
|
||||
isVideoEnabled: state.isVideoEnabled,
|
||||
unmutedVideoLimit: state.unmutedVideoLimit,
|
||||
isStream: state.isStream,
|
||||
isCreator: state.isCreator
|
||||
),
|
||||
topParticipants: topParticipants,
|
||||
participantCount: state.totalCount,
|
||||
activeSpeakers: activeSpeakers,
|
||||
groupCall: nil
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCache {
|
||||
public class Impl {
|
||||
private class Record {
|
||||
let context: AccountGroupCallContextImpl
|
||||
let subscribers = Bag<Void>()
|
||||
var removeTimer: SwiftSignalKit.Timer?
|
||||
|
||||
init(context: AccountGroupCallContextImpl) {
|
||||
self.context = context
|
||||
}
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private var contexts: [Int64: Record] = [:]
|
||||
|
||||
private let leaveDisposables = DisposableSet()
|
||||
|
||||
init(queue: Queue) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
public func get(account: Account, engine: TelegramEngine, peerId: EnginePeer.Id, isChannel: Bool, call: EngineGroupCallDescription) -> AccountGroupCallContextImpl.Proxy {
|
||||
let result: Record
|
||||
if let current = self.contexts[call.id] {
|
||||
result = current
|
||||
} else {
|
||||
let context = AccountGroupCallContextImpl(account: account, engine: engine, peerId: peerId, isChannel: isChannel, call: call)
|
||||
result = Record(context: context)
|
||||
self.contexts[call.id] = result
|
||||
}
|
||||
|
||||
let index = result.subscribers.add(Void())
|
||||
result.removeTimer?.invalidate()
|
||||
result.removeTimer = nil
|
||||
return AccountGroupCallContextImpl.Proxy(context: result.context, removed: { [weak self, weak result] in
|
||||
Queue.mainQueue().async {
|
||||
if let strongResult = result, let self, self.contexts[call.id] === strongResult {
|
||||
strongResult.subscribers.remove(index)
|
||||
if strongResult.subscribers.isEmpty {
|
||||
let removeTimer = SwiftSignalKit.Timer(timeout: 30, repeat: false, completion: { [weak self] in
|
||||
if let result = result, let self, self.contexts[call.id] === result, result.subscribers.isEmpty {
|
||||
self.contexts.removeValue(forKey: call.id)
|
||||
}
|
||||
}, queue: .mainQueue())
|
||||
strongResult.removeTimer = removeTimer
|
||||
removeTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func leaveInBackground(engine: TelegramEngine, id: Int64, accessHash: Int64, source: UInt32) {
|
||||
let disposable = engine.calls.leaveGroupCall(callId: id, accessHash: accessHash, source: source).start(completed: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let context = self.contexts[id] {
|
||||
context.context.participantsContext?.removeLocalPeerId()
|
||||
}
|
||||
})
|
||||
self.leaveDisposables.add(disposable)
|
||||
}
|
||||
}
|
||||
|
||||
let queue: Queue = .mainQueue()
|
||||
public let impl: QueueLocalObject<Impl>
|
||||
|
||||
public init() {
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue)
|
||||
})
|
||||
}
|
||||
}
|
@ -95,8 +95,8 @@ public final class CallKitIntegration {
|
||||
sharedProviderDelegate?.applyVoiceChatOutputMode(outputMode: outputMode)
|
||||
}
|
||||
|
||||
public func updateCallIsConference(uuid: UUID) {
|
||||
sharedProviderDelegate?.updateCallIsConference(uuid: uuid)
|
||||
public func updateCallIsConference(uuid: UUID, title: String) {
|
||||
sharedProviderDelegate?.updateCallIsConference(uuid: uuid, title: title)
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,12 +280,11 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
|
||||
self.provider.reportOutgoingCall(with: uuid, connectedAt: date)
|
||||
}
|
||||
|
||||
func updateCallIsConference(uuid: UUID) {
|
||||
func updateCallIsConference(uuid: UUID, title: String) {
|
||||
let update = CXCallUpdate()
|
||||
let handle = CXHandle(type: .generic, value: "\(uuid)")
|
||||
update.remoteHandle = handle
|
||||
//TODO:localize
|
||||
update.localizedCallerName = "Group Call"
|
||||
update.localizedCallerName = title
|
||||
update.supportsHolding = false
|
||||
update.supportsGrouping = false
|
||||
update.supportsUngrouping = false
|
||||
|
45
submodules/TelegramCallsUI/Sources/GroupCallLogs.swift
Normal file
45
submodules/TelegramCallsUI/Sources/GroupCallLogs.swift
Normal file
@ -0,0 +1,45 @@
|
||||
import Foundation
|
||||
import TelegramCore
|
||||
|
||||
public func groupCallLogsPath(account: Account) -> String {
|
||||
return account.basePath + "/group-calls"
|
||||
}
|
||||
|
||||
func cleanupGroupCallLogs(account: Account) {
|
||||
let path = groupCallLogsPath(account: account)
|
||||
let fileManager = FileManager.default
|
||||
if !fileManager.fileExists(atPath: path, isDirectory: nil) {
|
||||
try? fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
var oldest: [(URL, Date)] = []
|
||||
var count = 0
|
||||
if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) {
|
||||
for url in enumerator {
|
||||
if let url = url as? URL {
|
||||
if let date = (try? url.resourceValues(forKeys: Set([.contentModificationDateKey])))?.contentModificationDate {
|
||||
oldest.append((url, date))
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let callLogsLimit = 20
|
||||
if count > callLogsLimit {
|
||||
oldest.sort(by: { $0.1 > $1.1 })
|
||||
while oldest.count > callLogsLimit {
|
||||
try? fileManager.removeItem(atPath: oldest[oldest.count - 1].0.path)
|
||||
oldest.removeLast()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func allocateCallLogPath(account: Account) -> String {
|
||||
let path = groupCallLogsPath(account: account)
|
||||
|
||||
let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: path), withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let name = "log-\(Date())".replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: ":", with: "_")
|
||||
|
||||
return "\(path)/\(name).log"
|
||||
}
|
179
submodules/TelegramCallsUI/Sources/GroupCallScreencast.swift
Normal file
179
submodules/TelegramCallsUI/Sources/GroupCallScreencast.swift
Normal file
@ -0,0 +1,179 @@
|
||||
import Foundation
|
||||
import TelegramCore
|
||||
import TelegramVoip
|
||||
import SwiftSignalKit
|
||||
|
||||
protocol ScreencastContext: AnyObject {
|
||||
func addExternalAudioData(data: Data)
|
||||
func stop(account: Account, reportCallId: CallId?)
|
||||
func setRTCJoinResponse(clientParams: String)
|
||||
}
|
||||
|
||||
protocol ScreencastIPCContext: AnyObject {
|
||||
var isActive: Signal<Bool, NoError> { get }
|
||||
|
||||
func requestScreencast() -> Signal<(String, UInt32), NoError>?
|
||||
func setJoinResponse(clientParams: String)
|
||||
func disableScreencast(account: Account)
|
||||
}
|
||||
|
||||
final class ScreencastInProcessIPCContext: ScreencastIPCContext {
|
||||
private let isConference: Bool
|
||||
private let e2eContext: ConferenceCallE2EContext?
|
||||
|
||||
private let screencastBufferServerContext: IpcGroupCallBufferAppContext
|
||||
private var screencastCallContext: ScreencastContext?
|
||||
private let screencastCapturer: OngoingCallVideoCapturer
|
||||
private var screencastFramesDisposable: Disposable?
|
||||
private var screencastAudioDataDisposable: Disposable?
|
||||
|
||||
var isActive: Signal<Bool, NoError> {
|
||||
return self.screencastBufferServerContext.isActive
|
||||
}
|
||||
|
||||
init(basePath: String, isConference: Bool, e2eContext: ConferenceCallE2EContext?) {
|
||||
self.isConference = isConference
|
||||
self.e2eContext = e2eContext
|
||||
|
||||
let screencastBufferServerContext = IpcGroupCallBufferAppContext(basePath: basePath + "/broadcast-coordination")
|
||||
self.screencastBufferServerContext = screencastBufferServerContext
|
||||
let screencastCapturer = OngoingCallVideoCapturer(isCustom: true)
|
||||
self.screencastCapturer = screencastCapturer
|
||||
self.screencastFramesDisposable = (screencastBufferServerContext.frames
|
||||
|> deliverOnMainQueue).start(next: { [weak screencastCapturer] screencastFrame in
|
||||
guard let screencastCapturer = screencastCapturer else {
|
||||
return
|
||||
}
|
||||
guard let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: screencastFrame.0) else {
|
||||
return
|
||||
}
|
||||
screencastCapturer.injectSampleBuffer(sampleBuffer, rotation: screencastFrame.1, completion: {})
|
||||
})
|
||||
self.screencastAudioDataDisposable = (screencastBufferServerContext.audioData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||
Queue.mainQueue().async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.screencastCallContext?.addExternalAudioData(data: data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.screencastFramesDisposable?.dispose()
|
||||
self.screencastAudioDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
func requestScreencast() -> Signal<(String, UInt32), NoError>? {
|
||||
if self.screencastCallContext == nil {
|
||||
var encryptionContext: OngoingGroupCallEncryptionContext?
|
||||
if let e2eContext = self.e2eContext {
|
||||
encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: e2eContext.state, channelId: 1)
|
||||
} else if self.isConference {
|
||||
// Prevent non-encrypted conference calls
|
||||
encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: Atomic(value: ConferenceCallE2EContext.ContextStateHolder()), channelId: 1)
|
||||
}
|
||||
|
||||
let screencastCallContext = InProcessScreencastContext(
|
||||
context: OngoingGroupCallContext(
|
||||
audioSessionActive: .single(true),
|
||||
video: self.screencastCapturer,
|
||||
requestMediaChannelDescriptions: { _, _ in EmptyDisposable },
|
||||
rejoinNeeded: { },
|
||||
outgoingAudioBitrateKbit: nil,
|
||||
videoContentType: .screencast,
|
||||
enableNoiseSuppression: false,
|
||||
disableAudioInput: true,
|
||||
enableSystemMute: false,
|
||||
prioritizeVP8: false,
|
||||
logPath: "",
|
||||
onMutedSpeechActivityDetected: { _ in },
|
||||
isConference: self.isConference,
|
||||
audioIsActiveByDefault: true,
|
||||
isStream: false,
|
||||
sharedAudioDevice: nil,
|
||||
encryptionContext: encryptionContext
|
||||
)
|
||||
)
|
||||
self.screencastCallContext = screencastCallContext
|
||||
return screencastCallContext.joinPayload
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func setJoinResponse(clientParams: String) {
|
||||
if let screencastCallContext = self.screencastCallContext {
|
||||
screencastCallContext.setRTCJoinResponse(clientParams: clientParams)
|
||||
}
|
||||
}
|
||||
|
||||
func disableScreencast(account: Account) {
|
||||
if let screencastCallContext = self.screencastCallContext {
|
||||
self.screencastCallContext = nil
|
||||
screencastCallContext.stop(account: account, reportCallId: nil)
|
||||
|
||||
self.screencastBufferServerContext.stopScreencast()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ScreencastEmbeddedIPCContext: ScreencastIPCContext {
|
||||
private let serverContext: IpcGroupCallEmbeddedAppContext
|
||||
|
||||
var isActive: Signal<Bool, NoError> {
|
||||
return self.serverContext.isActive
|
||||
}
|
||||
|
||||
init(basePath: String) {
|
||||
self.serverContext = IpcGroupCallEmbeddedAppContext(basePath: basePath + "/embedded-broadcast-coordination")
|
||||
}
|
||||
|
||||
func requestScreencast() -> Signal<(String, UInt32), NoError>? {
|
||||
if let id = self.serverContext.startScreencast() {
|
||||
return self.serverContext.joinPayload
|
||||
|> filter { joinPayload -> Bool in
|
||||
return joinPayload.id == id
|
||||
}
|
||||
|> map { joinPayload -> (String, UInt32) in
|
||||
return (joinPayload.data, joinPayload.ssrc)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func setJoinResponse(clientParams: String) {
|
||||
self.serverContext.joinResponse = IpcGroupCallEmbeddedAppContext.JoinResponse(data: clientParams)
|
||||
}
|
||||
|
||||
func disableScreencast(account: Account) {
|
||||
self.serverContext.stopScreencast()
|
||||
}
|
||||
}
|
||||
|
||||
private final class InProcessScreencastContext: ScreencastContext {
|
||||
private let context: OngoingGroupCallContext
|
||||
|
||||
var joinPayload: Signal<(String, UInt32), NoError> {
|
||||
return self.context.joinPayload
|
||||
}
|
||||
|
||||
init(context: OngoingGroupCallContext) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
func addExternalAudioData(data: Data) {
|
||||
self.context.addExternalAudioData(data: data)
|
||||
}
|
||||
|
||||
func stop(account: Account, reportCallId: CallId?) {
|
||||
self.context.stop(account: account, reportCallId: reportCallId, debugLog: Promise())
|
||||
}
|
||||
|
||||
func setRTCJoinResponse(clientParams: String) {
|
||||
self.context.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false)
|
||||
self.context.setJoinResponse(payload: clientParams)
|
||||
}
|
||||
}
|
@ -241,6 +241,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
public let isOutgoing: Bool
|
||||
private let incomingConferenceSource: EngineMessage.Id?
|
||||
private let conferenceStableId: Int64?
|
||||
private var conferenceTitle: String?
|
||||
public var isVideo: Bool
|
||||
public var isVideoPossible: Bool
|
||||
private let enableStunMarking: Bool
|
||||
@ -483,7 +484,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
|
||||
let state: CallSessionState
|
||||
if let message = message {
|
||||
if let message {
|
||||
var foundAction: TelegramMediaAction?
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
@ -498,6 +499,21 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
} else {
|
||||
state = .ringing
|
||||
}
|
||||
|
||||
var conferenceTitle = "Group Call"
|
||||
if let peer = message.peers[message.id.peerId].flatMap(EnginePeer.init) {
|
||||
conferenceTitle = peer.compactDisplayTitle
|
||||
|
||||
let otherCount = conferenceCall.otherParticipants.filter({ $0 != peer.id }).count
|
||||
if otherCount != 0 {
|
||||
if otherCount == 1 {
|
||||
conferenceTitle.append(" and 1 other")
|
||||
} else {
|
||||
conferenceTitle.append(" and \(otherCount) others")
|
||||
}
|
||||
}
|
||||
}
|
||||
self.conferenceTitle = conferenceTitle
|
||||
} else {
|
||||
state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions())
|
||||
}
|
||||
@ -749,7 +765,8 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.localVideoEndpointId = nil
|
||||
self.remoteVideoEndpointId = nil
|
||||
|
||||
self.callKitIntegration?.updateCallIsConference(uuid: self.internalId)
|
||||
//TODO:localize
|
||||
self.callKitIntegration?.updateCallIsConference(uuid: self.internalId, title: self.conferenceTitle ?? "Group Call")
|
||||
}
|
||||
|
||||
func internal_markAsCanBeRemoved() {
|
||||
|
@ -19,242 +19,6 @@ import TemporaryCachedPeerDataManager
|
||||
import CallsEmoji
|
||||
import TdBinding
|
||||
|
||||
private extension GroupCallParticipantsContext.Participant {
|
||||
var allSsrcs: Set<UInt32> {
|
||||
var participantSsrcs = Set<UInt32>()
|
||||
if let ssrc = self.ssrc {
|
||||
participantSsrcs.insert(ssrc)
|
||||
}
|
||||
if let videoDescription = self.videoDescription {
|
||||
for group in videoDescription.ssrcGroups {
|
||||
for ssrc in group.ssrcs {
|
||||
participantSsrcs.insert(ssrc)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let presentationDescription = self.presentationDescription {
|
||||
for group in presentationDescription.ssrcGroups {
|
||||
for ssrc in group.ssrcs {
|
||||
participantSsrcs.insert(ssrc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return participantSsrcs
|
||||
}
|
||||
|
||||
var videoSsrcs: Set<UInt32> {
|
||||
var participantSsrcs = Set<UInt32>()
|
||||
if let videoDescription = self.videoDescription {
|
||||
for group in videoDescription.ssrcGroups {
|
||||
for ssrc in group.ssrcs {
|
||||
participantSsrcs.insert(ssrc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return participantSsrcs
|
||||
}
|
||||
|
||||
var presentationSsrcs: Set<UInt32> {
|
||||
var participantSsrcs = Set<UInt32>()
|
||||
if let presentationDescription = self.presentationDescription {
|
||||
for group in presentationDescription.ssrcGroups {
|
||||
for ssrc in group.ssrcs {
|
||||
participantSsrcs.insert(ssrc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return participantSsrcs
|
||||
}
|
||||
}
|
||||
|
||||
public final class AccountGroupCallContextImpl: AccountGroupCallContext {
|
||||
public final class Proxy {
|
||||
public let context: AccountGroupCallContextImpl
|
||||
let removed: () -> Void
|
||||
|
||||
public init(context: AccountGroupCallContextImpl, removed: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.removed = removed
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.removed()
|
||||
}
|
||||
|
||||
public func keep() {
|
||||
}
|
||||
}
|
||||
|
||||
var disposable: Disposable?
|
||||
public var participantsContext: GroupCallParticipantsContext?
|
||||
|
||||
private let panelDataPromise = Promise<GroupCallPanelData?>()
|
||||
public var panelData: Signal<GroupCallPanelData?, NoError> {
|
||||
return self.panelDataPromise.get()
|
||||
}
|
||||
|
||||
public init(account: Account, engine: TelegramEngine, peerId: PeerId?, isChannel: Bool, call: EngineGroupCallDescription) {
|
||||
self.panelDataPromise.set(.single(nil))
|
||||
let state = engine.calls.getGroupCallParticipants(reference: .id(id: call.id, accessHash: call.accessHash), offset: "", ssrcs: [], limit: 100, sortAscending: nil)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
let peer: Signal<EnginePeer?, NoError>
|
||||
if let peerId {
|
||||
peer = engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
} else {
|
||||
peer = .single(nil)
|
||||
}
|
||||
self.disposable = (combineLatest(queue: .mainQueue(),
|
||||
state,
|
||||
peer
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state, peer in
|
||||
guard let self, let state = state else {
|
||||
return
|
||||
}
|
||||
let context = engine.calls.groupCall(
|
||||
peerId: peerId,
|
||||
myPeerId: account.peerId,
|
||||
id: call.id,
|
||||
reference: .id(id: call.id, accessHash: call.accessHash),
|
||||
state: state,
|
||||
previousServiceState: nil
|
||||
)
|
||||
|
||||
self.participantsContext = context
|
||||
|
||||
if let peerId {
|
||||
self.panelDataPromise.set(combineLatest(queue: .mainQueue(),
|
||||
context.state,
|
||||
context.activeSpeakers
|
||||
)
|
||||
|> map { state, activeSpeakers -> GroupCallPanelData in
|
||||
var topParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||
for participant in state.participants {
|
||||
if topParticipants.count >= 3 {
|
||||
break
|
||||
}
|
||||
topParticipants.append(participant)
|
||||
}
|
||||
|
||||
var isChannel = false
|
||||
if let peer = peer, case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||
isChannel = true
|
||||
}
|
||||
|
||||
return GroupCallPanelData(
|
||||
peerId: peerId,
|
||||
isChannel: isChannel,
|
||||
info: GroupCallInfo(
|
||||
id: call.id,
|
||||
accessHash: call.accessHash,
|
||||
participantCount: state.totalCount,
|
||||
streamDcId: nil,
|
||||
title: state.title,
|
||||
scheduleTimestamp: state.scheduleTimestamp,
|
||||
subscribedToScheduled: state.subscribedToScheduled,
|
||||
recordingStartTimestamp: nil,
|
||||
sortAscending: state.sortAscending,
|
||||
defaultParticipantsAreMuted: state.defaultParticipantsAreMuted,
|
||||
isVideoEnabled: state.isVideoEnabled,
|
||||
unmutedVideoLimit: state.unmutedVideoLimit,
|
||||
isStream: state.isStream,
|
||||
isCreator: state.isCreator
|
||||
),
|
||||
topParticipants: topParticipants,
|
||||
participantCount: state.totalCount,
|
||||
activeSpeakers: activeSpeakers,
|
||||
groupCall: nil
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCache {
|
||||
public class Impl {
|
||||
private class Record {
|
||||
let context: AccountGroupCallContextImpl
|
||||
let subscribers = Bag<Void>()
|
||||
var removeTimer: SwiftSignalKit.Timer?
|
||||
|
||||
init(context: AccountGroupCallContextImpl) {
|
||||
self.context = context
|
||||
}
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private var contexts: [Int64: Record] = [:]
|
||||
|
||||
private let leaveDisposables = DisposableSet()
|
||||
|
||||
init(queue: Queue) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
public func get(account: Account, engine: TelegramEngine, peerId: PeerId, isChannel: Bool, call: EngineGroupCallDescription) -> AccountGroupCallContextImpl.Proxy {
|
||||
let result: Record
|
||||
if let current = self.contexts[call.id] {
|
||||
result = current
|
||||
} else {
|
||||
let context = AccountGroupCallContextImpl(account: account, engine: engine, peerId: peerId, isChannel: isChannel, call: call)
|
||||
result = Record(context: context)
|
||||
self.contexts[call.id] = result
|
||||
}
|
||||
|
||||
let index = result.subscribers.add(Void())
|
||||
result.removeTimer?.invalidate()
|
||||
result.removeTimer = nil
|
||||
return AccountGroupCallContextImpl.Proxy(context: result.context, removed: { [weak self, weak result] in
|
||||
Queue.mainQueue().async {
|
||||
if let strongResult = result, let self, self.contexts[call.id] === strongResult {
|
||||
strongResult.subscribers.remove(index)
|
||||
if strongResult.subscribers.isEmpty {
|
||||
let removeTimer = SwiftSignalKit.Timer(timeout: 30, repeat: false, completion: { [weak self] in
|
||||
if let result = result, let self, self.contexts[call.id] === result, result.subscribers.isEmpty {
|
||||
self.contexts.removeValue(forKey: call.id)
|
||||
}
|
||||
}, queue: .mainQueue())
|
||||
strongResult.removeTimer = removeTimer
|
||||
removeTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func leaveInBackground(engine: TelegramEngine, id: Int64, accessHash: Int64, source: UInt32) {
|
||||
let disposable = engine.calls.leaveGroupCall(callId: id, accessHash: accessHash, source: source).start(completed: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let context = self.contexts[id] {
|
||||
context.context.participantsContext?.removeLocalPeerId()
|
||||
}
|
||||
})
|
||||
self.leaveDisposables.add(disposable)
|
||||
}
|
||||
}
|
||||
|
||||
let queue: Queue = .mainQueue()
|
||||
public let impl: QueueLocalObject<Impl>
|
||||
|
||||
public init() {
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private extension PresentationGroupCallState {
|
||||
static func initialValue(myPeerId: PeerId, title: String?, scheduleTimestamp: Int32?, subscribedToScheduled: Bool) -> PresentationGroupCallState {
|
||||
return PresentationGroupCallState(
|
||||
@ -440,183 +204,6 @@ private extension CurrentImpl {
|
||||
}
|
||||
}
|
||||
|
||||
public func groupCallLogsPath(account: Account) -> String {
|
||||
return account.basePath + "/group-calls"
|
||||
}
|
||||
|
||||
private func cleanupGroupCallLogs(account: Account) {
|
||||
let path = groupCallLogsPath(account: account)
|
||||
let fileManager = FileManager.default
|
||||
if !fileManager.fileExists(atPath: path, isDirectory: nil) {
|
||||
try? fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
var oldest: [(URL, Date)] = []
|
||||
var count = 0
|
||||
if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) {
|
||||
for url in enumerator {
|
||||
if let url = url as? URL {
|
||||
if let date = (try? url.resourceValues(forKeys: Set([.contentModificationDateKey])))?.contentModificationDate {
|
||||
oldest.append((url, date))
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let callLogsLimit = 20
|
||||
if count > callLogsLimit {
|
||||
oldest.sort(by: { $0.1 > $1.1 })
|
||||
while oldest.count > callLogsLimit {
|
||||
try? fileManager.removeItem(atPath: oldest[oldest.count - 1].0.path)
|
||||
oldest.removeLast()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func allocateCallLogPath(account: Account) -> String {
|
||||
let path = groupCallLogsPath(account: account)
|
||||
|
||||
let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: path), withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let name = "log-\(Date())".replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: ":", with: "_")
|
||||
|
||||
return "\(path)/\(name).log"
|
||||
}
|
||||
|
||||
private protocol ScreencastIPCContext: AnyObject {
|
||||
var isActive: Signal<Bool, NoError> { get }
|
||||
|
||||
func requestScreencast() -> Signal<(String, UInt32), NoError>?
|
||||
func setJoinResponse(clientParams: String)
|
||||
func disableScreencast(account: Account)
|
||||
}
|
||||
|
||||
private final class ScreencastInProcessIPCContext: ScreencastIPCContext {
|
||||
private let isConference: Bool
|
||||
|
||||
private let screencastBufferServerContext: IpcGroupCallBufferAppContext
|
||||
private var screencastCallContext: ScreencastContext?
|
||||
private let screencastCapturer: OngoingCallVideoCapturer
|
||||
private var screencastFramesDisposable: Disposable?
|
||||
private var screencastAudioDataDisposable: Disposable?
|
||||
|
||||
var isActive: Signal<Bool, NoError> {
|
||||
return self.screencastBufferServerContext.isActive
|
||||
}
|
||||
|
||||
init(basePath: String, isConference: Bool) {
|
||||
self.isConference = isConference
|
||||
|
||||
let screencastBufferServerContext = IpcGroupCallBufferAppContext(basePath: basePath + "/broadcast-coordination")
|
||||
self.screencastBufferServerContext = screencastBufferServerContext
|
||||
let screencastCapturer = OngoingCallVideoCapturer(isCustom: true)
|
||||
self.screencastCapturer = screencastCapturer
|
||||
self.screencastFramesDisposable = (screencastBufferServerContext.frames
|
||||
|> deliverOnMainQueue).start(next: { [weak screencastCapturer] screencastFrame in
|
||||
guard let screencastCapturer = screencastCapturer else {
|
||||
return
|
||||
}
|
||||
guard let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: screencastFrame.0) else {
|
||||
return
|
||||
}
|
||||
screencastCapturer.injectSampleBuffer(sampleBuffer, rotation: screencastFrame.1, completion: {})
|
||||
})
|
||||
self.screencastAudioDataDisposable = (screencastBufferServerContext.audioData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||
Queue.mainQueue().async {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.screencastCallContext?.addExternalAudioData(data: data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.screencastFramesDisposable?.dispose()
|
||||
self.screencastAudioDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
func requestScreencast() -> Signal<(String, UInt32), NoError>? {
|
||||
if self.screencastCallContext == nil {
|
||||
let screencastCallContext = InProcessScreencastContext(
|
||||
context: OngoingGroupCallContext(
|
||||
audioSessionActive: .single(true),
|
||||
video: self.screencastCapturer,
|
||||
requestMediaChannelDescriptions: { _, _ in EmptyDisposable },
|
||||
rejoinNeeded: { },
|
||||
outgoingAudioBitrateKbit: nil,
|
||||
videoContentType: .screencast,
|
||||
enableNoiseSuppression: false,
|
||||
disableAudioInput: true,
|
||||
enableSystemMute: false,
|
||||
prioritizeVP8: false,
|
||||
logPath: "",
|
||||
onMutedSpeechActivityDetected: { _ in },
|
||||
isConference: self.isConference,
|
||||
audioIsActiveByDefault: true,
|
||||
isStream: false,
|
||||
sharedAudioDevice: nil,
|
||||
encryptionContext: nil
|
||||
)
|
||||
)
|
||||
self.screencastCallContext = screencastCallContext
|
||||
return screencastCallContext.joinPayload
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func setJoinResponse(clientParams: String) {
|
||||
if let screencastCallContext = self.screencastCallContext {
|
||||
screencastCallContext.setRTCJoinResponse(clientParams: clientParams)
|
||||
}
|
||||
}
|
||||
|
||||
func disableScreencast(account: Account) {
|
||||
if let screencastCallContext = self.screencastCallContext {
|
||||
self.screencastCallContext = nil
|
||||
screencastCallContext.stop(account: account, reportCallId: nil)
|
||||
|
||||
self.screencastBufferServerContext.stopScreencast()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScreencastEmbeddedIPCContext: ScreencastIPCContext {
|
||||
private let serverContext: IpcGroupCallEmbeddedAppContext
|
||||
|
||||
var isActive: Signal<Bool, NoError> {
|
||||
return self.serverContext.isActive
|
||||
}
|
||||
|
||||
init(basePath: String) {
|
||||
self.serverContext = IpcGroupCallEmbeddedAppContext(basePath: basePath + "/embedded-broadcast-coordination")
|
||||
}
|
||||
|
||||
func requestScreencast() -> Signal<(String, UInt32), NoError>? {
|
||||
if let id = self.serverContext.startScreencast() {
|
||||
return self.serverContext.joinPayload
|
||||
|> filter { joinPayload -> Bool in
|
||||
return joinPayload.id == id
|
||||
}
|
||||
|> map { joinPayload -> (String, UInt32) in
|
||||
return (joinPayload.data, joinPayload.ssrc)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func setJoinResponse(clientParams: String) {
|
||||
self.serverContext.joinResponse = IpcGroupCallEmbeddedAppContext.JoinResponse(data: clientParams)
|
||||
}
|
||||
|
||||
func disableScreencast(account: Account) {
|
||||
self.serverContext.stopScreencast()
|
||||
}
|
||||
}
|
||||
|
||||
private final class PendingConferenceInvitationContext {
|
||||
enum State {
|
||||
case ringing
|
||||
@ -748,8 +335,8 @@ private final class ConferenceCallE2EContextStateImpl: ConferenceCallE2EContextS
|
||||
return self.call.takeOutgoingBroadcastBlocks()
|
||||
}
|
||||
|
||||
func encrypt(message: Data) -> Data? {
|
||||
return self.call.encrypt(message)
|
||||
func encrypt(message: Data, channelId: Int32) -> Data? {
|
||||
return self.call.encrypt(message, channelId: channelId)
|
||||
}
|
||||
|
||||
func decrypt(message: Data, userId: Int64) -> Data? {
|
||||
@ -757,6 +344,25 @@ private final class ConferenceCallE2EContextStateImpl: ConferenceCallE2EContextS
|
||||
}
|
||||
}
|
||||
|
||||
class OngoingGroupCallEncryptionContextImpl: OngoingGroupCallEncryptionContext {
|
||||
private let e2eCall: Atomic<ConferenceCallE2EContext.ContextStateHolder>
|
||||
private let channelId: Int32
|
||||
|
||||
init(e2eCall: Atomic<ConferenceCallE2EContext.ContextStateHolder>, channelId: Int32) {
|
||||
self.e2eCall = e2eCall
|
||||
self.channelId = channelId
|
||||
}
|
||||
|
||||
func encrypt(message: Data) -> Data? {
|
||||
let channelId = self.channelId
|
||||
return self.e2eCall.with({ $0.state?.encrypt(message: message, channelId: channelId) })
|
||||
}
|
||||
|
||||
func decrypt(message: Data, userId: Int64) -> Data? {
|
||||
return self.e2eCall.with({ $0.state?.decrypt(message: message, userId: userId) })
|
||||
}
|
||||
}
|
||||
|
||||
public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
private enum InternalState {
|
||||
case requesting
|
||||
@ -1505,9 +1111,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
self.requestCall(movingFromBroadcastToRtc: false)
|
||||
}
|
||||
|
||||
var useIPCContext = "".isEmpty
|
||||
if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_use_inprocess_screencast"] != nil {
|
||||
useIPCContext = false
|
||||
var useIPCContext = false
|
||||
if let data = self.accountContext.currentAppConfiguration.with({ $0 }).data, let value = data["ios_use_inprocess_screencast"] as? Double {
|
||||
useIPCContext = value != 0.0
|
||||
}
|
||||
|
||||
let embeddedBroadcastImplementationTypePath = self.accountContext.sharedContext.basePath + "/broadcast-coordination-type"
|
||||
@ -1517,7 +1123,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
screencastIPCContext = ScreencastEmbeddedIPCContext(basePath: self.accountContext.sharedContext.basePath)
|
||||
let _ = try? "ipc".write(toFile: embeddedBroadcastImplementationTypePath, atomically: true, encoding: .utf8)
|
||||
} else {
|
||||
screencastIPCContext = ScreencastInProcessIPCContext(basePath: self.accountContext.sharedContext.basePath, isConference: self.isConference)
|
||||
screencastIPCContext = ScreencastInProcessIPCContext(basePath: self.accountContext.sharedContext.basePath, isConference: self.isConference, e2eContext: self.e2eContext)
|
||||
let _ = try? "legacy".write(toFile: embeddedBroadcastImplementationTypePath, atomically: true, encoding: .utf8)
|
||||
}
|
||||
self.screencastIPCContext = screencastIPCContext
|
||||
@ -2093,28 +1699,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
audioIsActiveByDefault = false
|
||||
}
|
||||
|
||||
class OngoingGroupCallEncryptionContextImpl: OngoingGroupCallEncryptionContext {
|
||||
private let e2eCall: Atomic<ConferenceCallE2EContext.ContextStateHolder>
|
||||
|
||||
init(e2eCall: Atomic<ConferenceCallE2EContext.ContextStateHolder>) {
|
||||
self.e2eCall = e2eCall
|
||||
}
|
||||
|
||||
func encrypt(message: Data) -> Data? {
|
||||
return self.e2eCall.with({ $0.state?.encrypt(message: message) })
|
||||
}
|
||||
|
||||
func decrypt(message: Data, userId: Int64) -> Data? {
|
||||
return self.e2eCall.with({ $0.state?.decrypt(message: message, userId: userId) })
|
||||
}
|
||||
}
|
||||
|
||||
var encryptionContext: OngoingGroupCallEncryptionContext?
|
||||
if let e2eContext = self.e2eContext {
|
||||
encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: e2eContext.state)
|
||||
encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: e2eContext.state, channelId: 0)
|
||||
} else if self.isConference {
|
||||
// Prevent non-encrypted conference calls
|
||||
encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: Atomic(value: ConferenceCallE2EContext.ContextStateHolder()))
|
||||
encryptionContext = OngoingGroupCallEncryptionContextImpl(e2eCall: Atomic(value: ConferenceCallE2EContext.ContextStateHolder()), channelId: 0)
|
||||
}
|
||||
|
||||
var prioritizeVP8 = false
|
||||
@ -4200,37 +3790,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
}
|
||||
|
||||
private protocol ScreencastContext: AnyObject {
|
||||
func addExternalAudioData(data: Data)
|
||||
func stop(account: Account, reportCallId: CallId?)
|
||||
func setRTCJoinResponse(clientParams: String)
|
||||
}
|
||||
|
||||
private final class InProcessScreencastContext: ScreencastContext {
|
||||
private let context: OngoingGroupCallContext
|
||||
|
||||
var joinPayload: Signal<(String, UInt32), NoError> {
|
||||
return self.context.joinPayload
|
||||
}
|
||||
|
||||
init(context: OngoingGroupCallContext) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
func addExternalAudioData(data: Data) {
|
||||
self.context.addExternalAudioData(data: data)
|
||||
}
|
||||
|
||||
func stop(account: Account, reportCallId: CallId?) {
|
||||
self.context.stop(account: account, reportCallId: reportCallId, debugLog: Promise())
|
||||
}
|
||||
|
||||
func setRTCJoinResponse(clientParams: String) {
|
||||
self.context.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false)
|
||||
self.context.setJoinResponse(payload: clientParams)
|
||||
}
|
||||
}
|
||||
|
||||
public final class TelegramE2EEncryptionProviderImpl: TelegramE2EEncryptionProvider {
|
||||
public static let shared = TelegramE2EEncryptionProviderImpl()
|
||||
|
||||
|
@ -1,45 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
final class ReactionStrip: ASDisplayNode {
|
||||
private var labelValues: [String] = []
|
||||
private var labelNodes: [ImmediateTextNode] = []
|
||||
|
||||
var selected: ((String) -> Void)?
|
||||
|
||||
override init() {
|
||||
self.labelValues = ["🧡", "🎆", "🎈", "🎉", "👍", "👎", "💩", "💸", "😂"]
|
||||
|
||||
super.init()
|
||||
|
||||
for labelValue in self.labelValues {
|
||||
let labelNode = ImmediateTextNode()
|
||||
labelNode.attributedText = NSAttributedString(string: labelValue, font: Font.regular(20.0), textColor: .black)
|
||||
self.labelNodes.append(labelNode)
|
||||
self.addSubnode(labelNode)
|
||||
labelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.labelTapGesture(_:))))
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func labelTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
for i in 0 ..< self.labelNodes.count {
|
||||
if self.labelNodes[i].view === recognizer.view {
|
||||
self.selected?(self.labelValues[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize) {
|
||||
var labelOrigin = CGPoint(x: 0.0, y: 0.0)
|
||||
for labelNode in self.labelNodes {
|
||||
let labelSize = labelNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
labelNode.frame = CGRect(origin: labelOrigin, size: labelSize)
|
||||
labelOrigin.x += labelSize.width + 10.0
|
||||
}
|
||||
}
|
||||
}
|
@ -156,6 +156,11 @@ extension VideoChatScreenComponent.View {
|
||||
}
|
||||
|
||||
let canManageCall = callState.canManageCall
|
||||
|
||||
var isConference = false
|
||||
if case let .group(groupCall) = currentCall {
|
||||
isConference = groupCall.isConference
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
@ -175,35 +180,6 @@ extension VideoChatScreenComponent.View {
|
||||
}
|
||||
}
|
||||
|
||||
/*if case let .group(groupCall) = currentCall, let encryptionKey = groupCall.encryptionKeyValue {
|
||||
//TODO:localize
|
||||
let emojiKey = resolvedEmojiKey(data: encryptionKey)
|
||||
items.append(.action(ContextMenuActionItem(text: "Encryption Key", textLayout: .secondLineWithValue(emojiKey.joined(separator: "")), icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Lock"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
c?.dismiss(completion: nil)
|
||||
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
|
||||
let alertController = componentAlertController(
|
||||
theme: AlertControllerTheme(presentationTheme: defaultDarkPresentationTheme, fontSize: .regular),
|
||||
content: AnyComponent(EmojiKeyAlertComponet(
|
||||
theme: defaultDarkPresentationTheme,
|
||||
emojiKey: emojiKey,
|
||||
title: "This call is end-to-end encrypted",
|
||||
text: "If the emojis on everyone's screens are the same, this call is 100% secure."
|
||||
)),
|
||||
actions: [ComponentAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})],
|
||||
actionLayout: .horizontal
|
||||
)
|
||||
|
||||
environment.controller()?.present(alertController, in: .window(.root))
|
||||
})))
|
||||
items.append(.separator)
|
||||
}*/
|
||||
|
||||
if let (availableOutputs, currentOutput) = self.audioOutputState, availableOutputs.count > 1 {
|
||||
var currentOutputTitle = ""
|
||||
for output in availableOutputs {
|
||||
@ -233,7 +209,7 @@ extension VideoChatScreenComponent.View {
|
||||
})))
|
||||
}
|
||||
|
||||
if canManageCall {
|
||||
if canManageCall && !isConference {
|
||||
let text: String
|
||||
if case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_EditTitle
|
||||
@ -356,7 +332,7 @@ extension VideoChatScreenComponent.View {
|
||||
})))
|
||||
}
|
||||
|
||||
if case let .group(groupCall) = currentCall, !groupCall.isConference, callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) {
|
||||
if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) {
|
||||
if currentCall.hasScreencast {
|
||||
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_StopScreenSharing, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor)
|
||||
@ -375,7 +351,7 @@ extension VideoChatScreenComponent.View {
|
||||
}
|
||||
}
|
||||
|
||||
if canManageCall {
|
||||
if canManageCall && !isConference {
|
||||
if let recordingStartTimestamp = callState.recordingStartTimestamp {
|
||||
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak self] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
@ -12,7 +12,7 @@ public protocol ConferenceCallE2EContextState: AnyObject {
|
||||
|
||||
func takeOutgoingBroadcastBlocks() -> [Data]
|
||||
|
||||
func encrypt(message: Data) -> Data?
|
||||
func encrypt(message: Data, channelId: Int32) -> Data?
|
||||
func decrypt(message: Data, userId: Int64) -> Data?
|
||||
}
|
||||
|
||||
|
@ -2165,7 +2165,14 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
let internalId = CallSessionManager.getStableIncomingUUID(peerId: fromPeerId.id._internalGetInt64Value(), messageId: messageId.id)
|
||||
|
||||
//TODO:localize
|
||||
let displayTitle: "\(fromTitle)"
|
||||
var displayTitle = "\(fromTitle)"
|
||||
if let memberCountString = payloadJson["member_count"] as? String, let memberCount = Int(memberCountString) {
|
||||
if memberCount == 1 {
|
||||
displayTitle.append(" and 1 other")
|
||||
} else {
|
||||
displayTitle.append(" and \(memberCount) others")
|
||||
}
|
||||
}
|
||||
|
||||
callKitIntegration.reportIncomingCall(
|
||||
uuid: internalId,
|
||||
|
@ -42,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (nullable NSData *)generateRemoveParticipantsBlock:(NSArray<NSNumber *> *)participantIds;
|
||||
|
||||
- (nullable NSData *)encrypt:(NSData *)message;
|
||||
- (nullable NSData *)encrypt:(NSData *)message channelId:(int32_t)channelId;
|
||||
- (nullable NSData *)decrypt:(NSData *)message userId:(int64_t)userId;
|
||||
|
||||
@end
|
||||
|
@ -286,9 +286,9 @@ static NSString *hexStringFromData(NSData *data) {
|
||||
return [[NSData alloc] initWithBytes:result.value().data() length:result.value().size()];
|
||||
}
|
||||
|
||||
- (nullable NSData *)encrypt:(NSData *)message {
|
||||
- (nullable NSData *)encrypt:(NSData *)message channelId:(int32_t)channelId {
|
||||
std::string mappedMessage((uint8_t *)message.bytes, ((uint8_t *)message.bytes) + message.length);
|
||||
auto result = tde2e_api::call_encrypt(_callId, 0, mappedMessage);
|
||||
auto result = tde2e_api::call_encrypt(_callId, channelId, mappedMessage);
|
||||
if (!result.is_ok()) {
|
||||
return nil;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user