mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 09:20:08 +00:00
Conference calls
This commit is contained in:
parent
b90065451b
commit
2393424bde
@ -111,6 +111,7 @@ public final class ContactSelectionControllerParams {
|
|||||||
public let requirePhoneNumbers: Bool
|
public let requirePhoneNumbers: Bool
|
||||||
public let allowChannelsInSearch: Bool
|
public let allowChannelsInSearch: Bool
|
||||||
public let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
|
public let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
|
||||||
|
public let isPeerEnabled: (ContactListPeer) -> Bool
|
||||||
public let openProfile: ((EnginePeer) -> Void)?
|
public let openProfile: ((EnginePeer) -> Void)?
|
||||||
public let sendMessage: ((EnginePeer) -> Void)?
|
public let sendMessage: ((EnginePeer) -> Void)?
|
||||||
|
|
||||||
@ -127,6 +128,7 @@ public final class ContactSelectionControllerParams {
|
|||||||
requirePhoneNumbers: Bool = false,
|
requirePhoneNumbers: Bool = false,
|
||||||
allowChannelsInSearch: Bool = false,
|
allowChannelsInSearch: Bool = false,
|
||||||
confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) },
|
confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) },
|
||||||
|
isPeerEnabled: @escaping (ContactListPeer) -> Bool = { _ in true },
|
||||||
openProfile: ((EnginePeer) -> Void)? = nil,
|
openProfile: ((EnginePeer) -> Void)? = nil,
|
||||||
sendMessage: ((EnginePeer) -> Void)? = nil
|
sendMessage: ((EnginePeer) -> Void)? = nil
|
||||||
) {
|
) {
|
||||||
@ -142,6 +144,7 @@ public final class ContactSelectionControllerParams {
|
|||||||
self.requirePhoneNumbers = requirePhoneNumbers
|
self.requirePhoneNumbers = requirePhoneNumbers
|
||||||
self.allowChannelsInSearch = allowChannelsInSearch
|
self.allowChannelsInSearch = allowChannelsInSearch
|
||||||
self.confirmation = confirmation
|
self.confirmation = confirmation
|
||||||
|
self.isPeerEnabled = isPeerEnabled
|
||||||
self.openProfile = openProfile
|
self.openProfile = openProfile
|
||||||
self.sendMessage = sendMessage
|
self.sendMessage = sendMessage
|
||||||
}
|
}
|
||||||
|
|||||||
@ -550,10 +550,6 @@ public enum PresentationCurrentCall: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum JoinConferenceCallMode {
|
|
||||||
case joining
|
|
||||||
}
|
|
||||||
|
|
||||||
public protocol PresentationCallManager: AnyObject {
|
public protocol PresentationCallManager: AnyObject {
|
||||||
var currentCallSignal: Signal<PresentationCall?, NoError> { get }
|
var currentCallSignal: Signal<PresentationCall?, NoError> { get }
|
||||||
var currentGroupCallSignal: Signal<VideoChatCall?, NoError> { get }
|
var currentGroupCallSignal: Signal<VideoChatCall?, NoError> { get }
|
||||||
@ -568,6 +564,6 @@ public protocol PresentationCallManager: AnyObject {
|
|||||||
accountContext: AccountContext,
|
accountContext: AccountContext,
|
||||||
initialCall: EngineGroupCallDescription,
|
initialCall: EngineGroupCallDescription,
|
||||||
reference: InternalGroupCallReference,
|
reference: InternalGroupCallReference,
|
||||||
mode: JoinConferenceCallMode
|
beginWithVideo: Bool
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -231,7 +231,7 @@ public final class CallListController: TelegramBaseController {
|
|||||||
isStream: false
|
isStream: false
|
||||||
),
|
),
|
||||||
reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash),
|
reference: .id(id: call.callInfo.id, accessHash: call.callInfo.accessHash),
|
||||||
mode: .joining
|
beginWithVideo: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -692,6 +692,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||||||
|
|
||||||
public func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
|
public func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
|
||||||
var reallyHighlighted = self.isHighlighted
|
var reallyHighlighted = self.isHighlighted
|
||||||
|
if let item = self.item, !item.enabled {
|
||||||
|
reallyHighlighted = false
|
||||||
|
}
|
||||||
let highlightProgress: CGFloat = self.item?.itemHighlighting?.progress ?? 1.0
|
let highlightProgress: CGFloat = self.item?.itemHighlighting?.progress ?? 1.0
|
||||||
if let item = self.item {
|
if let item = self.item {
|
||||||
switch item.peer {
|
switch item.peer {
|
||||||
@ -1649,6 +1652,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||||||
actionButtonNode.setImage(actionButton.image, for: .normal)
|
actionButtonNode.setImage(actionButton.image, for: .normal)
|
||||||
transition.updateFrame(node: actionButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 12.0 - actionButtonImage.size.width - offset, y: floor((nodeLayout.contentSize.height - actionButtonImage.size.height) / 2.0)), size: actionButtonImage.size))
|
transition.updateFrame(node: actionButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 12.0 - actionButtonImage.size.width - offset, y: floor((nodeLayout.contentSize.height - actionButtonImage.size.height) / 2.0)), size: actionButtonImage.size))
|
||||||
|
|
||||||
|
actionButtonNode.isEnabled = item.enabled
|
||||||
|
actionButtonNode.alpha = item.enabled ? 1.0 : 0.4
|
||||||
|
|
||||||
offset += actionButtonImage.size.width + 12.0
|
offset += actionButtonImage.size.width + 12.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -486,7 +486,7 @@ public final class CallController: ViewController {
|
|||||||
var disablePeerIds: [EnginePeer.Id] = []
|
var disablePeerIds: [EnginePeer.Id] = []
|
||||||
disablePeerIds.append(self.call.context.account.peerId)
|
disablePeerIds.append(self.call.context.account.peerId)
|
||||||
disablePeerIds.append(self.call.peerId)
|
disablePeerIds.append(self.call.peerId)
|
||||||
let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, completion: { [weak self] peers in
|
let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, shareLink: nil, completion: { [weak self] peers in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -497,15 +497,18 @@ public final class CallController: ViewController {
|
|||||||
self.push(controller)
|
self.push(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], completion: @escaping ([(id: EnginePeer.Id, isVideo: Bool)]) -> Void) -> ViewController {
|
static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], shareLink: (() -> Void)?, completion: @escaping ([(id: EnginePeer.Id, isVideo: Bool)]) -> Void) -> ViewController {
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
|
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||||
|
|
||||||
var options: [ContactListAdditionalOption] = []
|
var options: [ContactListAdditionalOption] = []
|
||||||
|
var openShareLinkImpl: (() -> Void)?
|
||||||
|
if shareLink != nil {
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
options.append(ContactListAdditionalOption(title: "Share Call Link", icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: {
|
options.append(ContactListAdditionalOption(title: "Share Call Link", icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: {
|
||||||
//TODO:release
|
openShareLinkImpl?()
|
||||||
}, clearHighlightAutomatically: false))
|
}, clearHighlightAutomatically: false))
|
||||||
|
}
|
||||||
|
|
||||||
let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
|
let controller = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
|
||||||
context: context,
|
context: context,
|
||||||
@ -534,9 +537,32 @@ public final class CallController: ViewController {
|
|||||||
default:
|
default:
|
||||||
return .single(false)
|
return .single(false)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
isPeerEnabled: { peer in
|
||||||
|
switch peer {
|
||||||
|
case let .peer(peer, _, _):
|
||||||
|
let peer = EnginePeer(peer)
|
||||||
|
guard case let .user(user) = peer else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if disablePeerIds.contains(user.id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if user.botInfo != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
openShareLinkImpl = { [weak controller] in
|
||||||
|
controller?.dismiss()
|
||||||
|
shareLink?()
|
||||||
|
}
|
||||||
|
|
||||||
controller.navigationPresentation = .modal
|
controller.navigationPresentation = .modal
|
||||||
let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in
|
let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in
|
||||||
guard let result, let peer = result.0.first, case let .peer(peer, _, _) = peer else {
|
guard let result, let peer = result.0.first, case let .peer(peer, _, _) = peer else {
|
||||||
|
|||||||
@ -1026,6 +1026,7 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
keyPair: keyPair,
|
keyPair: keyPair,
|
||||||
conferenceSourceId: self.internalId,
|
conferenceSourceId: self.internalId,
|
||||||
isConference: true,
|
isConference: true,
|
||||||
|
beginWithVideo: false,
|
||||||
sharedAudioContext: self.sharedAudioContext
|
sharedAudioContext: self.sharedAudioContext
|
||||||
)
|
)
|
||||||
self.conferenceCallImpl = conferenceCall
|
self.conferenceCallImpl = conferenceCall
|
||||||
|
|||||||
@ -850,6 +850,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||||||
keyPair: nil,
|
keyPair: nil,
|
||||||
conferenceSourceId: nil,
|
conferenceSourceId: nil,
|
||||||
isConference: false,
|
isConference: false,
|
||||||
|
beginWithVideo: false,
|
||||||
sharedAudioContext: nil
|
sharedAudioContext: nil
|
||||||
)
|
)
|
||||||
call.schedule(timestamp: timestamp)
|
call.schedule(timestamp: timestamp)
|
||||||
@ -1076,6 +1077,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||||||
keyPair: nil,
|
keyPair: nil,
|
||||||
conferenceSourceId: nil,
|
conferenceSourceId: nil,
|
||||||
isConference: false,
|
isConference: false,
|
||||||
|
beginWithVideo: false,
|
||||||
sharedAudioContext: nil
|
sharedAudioContext: nil
|
||||||
)
|
)
|
||||||
self.updateCurrentGroupCall(.group(call))
|
self.updateCurrentGroupCall(.group(call))
|
||||||
@ -1085,16 +1087,13 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||||||
accountContext: AccountContext,
|
accountContext: AccountContext,
|
||||||
initialCall: EngineGroupCallDescription,
|
initialCall: EngineGroupCallDescription,
|
||||||
reference: InternalGroupCallReference,
|
reference: InternalGroupCallReference,
|
||||||
mode: JoinConferenceCallMode
|
beginWithVideo: Bool
|
||||||
) {
|
) {
|
||||||
let keyPair: TelegramKeyPair
|
let keyPair: TelegramKeyPair
|
||||||
switch mode {
|
|
||||||
case .joining:
|
|
||||||
guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else {
|
guard let keyPairValue = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
keyPair = keyPairValue
|
keyPair = keyPairValue
|
||||||
}
|
|
||||||
|
|
||||||
let call = PresentationGroupCallImpl(
|
let call = PresentationGroupCallImpl(
|
||||||
accountContext: accountContext,
|
accountContext: accountContext,
|
||||||
@ -1111,6 +1110,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
|||||||
keyPair: keyPair,
|
keyPair: keyPair,
|
||||||
conferenceSourceId: nil,
|
conferenceSourceId: nil,
|
||||||
isConference: true,
|
isConference: true,
|
||||||
|
beginWithVideo: beginWithVideo,
|
||||||
sharedAudioContext: nil
|
sharedAudioContext: nil
|
||||||
)
|
)
|
||||||
self.updateCurrentGroupCall(.group(call))
|
self.updateCurrentGroupCall(.group(call))
|
||||||
|
|||||||
@ -621,54 +621,98 @@ private final class PendingConferenceInvitationContext {
|
|||||||
case ringing
|
case ringing
|
||||||
}
|
}
|
||||||
|
|
||||||
private let callSessionManager: CallSessionManager
|
private let engine: TelegramEngine
|
||||||
private var requestDisposable: Disposable?
|
private var requestDisposable: Disposable?
|
||||||
private var stateDisposable: Disposable?
|
private var stateDisposable: Disposable?
|
||||||
private var internalId: CallSessionInternalId?
|
private(set) var messageId: EngineMessage.Id?
|
||||||
|
|
||||||
|
private var hadMessage: Bool = false
|
||||||
private var didNotifyEnded: Bool = false
|
private var didNotifyEnded: Bool = false
|
||||||
|
|
||||||
init(callSessionManager: CallSessionManager, groupCall: GroupCallReference, peerId: PeerId, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) {
|
init(engine: TelegramEngine, reference: InternalGroupCallReference, peerId: PeerId, isVideo: Bool, onStateUpdated: @escaping (State) -> Void, onEnded: @escaping (Bool) -> Void) {
|
||||||
self.callSessionManager = callSessionManager
|
self.engine = engine
|
||||||
|
self.requestDisposable = (engine.calls.inviteConferenceCallParticipant(reference: reference, peerId: peerId, isVideo: isVideo).startStrict(next: { [weak self] messageId in
|
||||||
preconditionFailure()
|
|
||||||
|
|
||||||
/*self.requestDisposable = (callSessionManager.request(peerId: peerId, isVideo: false, enableVideo: true, conferenceCall: (groupCall, encryptionKey))
|
|
||||||
|> deliverOnMainQueue).startStrict(next: { [weak self] internalId in
|
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.internalId = internalId
|
guard let messageId else {
|
||||||
|
|
||||||
self.stateDisposable = (self.callSessionManager.callState(internalId: internalId)
|
|
||||||
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch state.state {
|
|
||||||
case let .requesting(ringing, _):
|
|
||||||
if ringing {
|
|
||||||
onStateUpdated(.ringing)
|
|
||||||
}
|
|
||||||
case let .dropping(reason), let .terminated(_, reason, _):
|
|
||||||
if !self.didNotifyEnded {
|
if !self.didNotifyEnded {
|
||||||
self.didNotifyEnded = true
|
self.didNotifyEnded = true
|
||||||
onEnded(reason == .ended(.switchedToConference))
|
onEnded(false)
|
||||||
}
|
}
|
||||||
default:
|
return
|
||||||
|
}
|
||||||
|
self.messageId = messageId
|
||||||
|
|
||||||
|
onStateUpdated(.ringing)
|
||||||
|
|
||||||
|
let timeout: Double = 30.0
|
||||||
|
let timerSignal = Signal<Void, NoError>.single(Void()) |> then(
|
||||||
|
Signal<Void, NoError>.single(Void())
|
||||||
|
|> delay(1.0, queue: .mainQueue())
|
||||||
|
) |> restart
|
||||||
|
|
||||||
|
let startTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
self.stateDisposable = (combineLatest(queue: .mainQueue(),
|
||||||
|
engine.data.subscribe(
|
||||||
|
TelegramEngine.EngineData.Item.Messages.Message(id: messageId)
|
||||||
|
),
|
||||||
|
timerSignal
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).startStrict(next: { [weak self] message, _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let message {
|
||||||
|
self.hadMessage = true
|
||||||
|
if message.timestamp + Int32(timeout) <= Int32(Date().timeIntervalSince1970) {
|
||||||
|
if !self.didNotifyEnded {
|
||||||
|
self.didNotifyEnded = true
|
||||||
|
onEnded(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var isActive = false
|
||||||
|
var isAccepted = false
|
||||||
|
var foundAction: TelegramMediaAction?
|
||||||
|
for media in message.media {
|
||||||
|
if let action = media as? TelegramMediaAction {
|
||||||
|
foundAction = action
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let action = foundAction, case let .conferenceCall(conferenceCall) = action.action {
|
||||||
|
if conferenceCall.flags.contains(.isMissed) || conferenceCall.duration != nil {
|
||||||
|
} else {
|
||||||
|
if conferenceCall.flags.contains(.isActive) {
|
||||||
|
isAccepted = true
|
||||||
|
} else {
|
||||||
|
isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isActive {
|
||||||
|
if !self.didNotifyEnded {
|
||||||
|
self.didNotifyEnded = true
|
||||||
|
onEnded(isAccepted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.hadMessage || CFAbsoluteTimeGetCurrent() > startTime + 1.0 {
|
||||||
|
if !self.didNotifyEnded {
|
||||||
|
self.didNotifyEnded = true
|
||||||
|
onEnded(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})*/
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.requestDisposable?.dispose()
|
self.requestDisposable?.dispose()
|
||||||
self.stateDisposable?.dispose()
|
self.stateDisposable?.dispose()
|
||||||
|
|
||||||
if let internalId = self.internalId {
|
|
||||||
self.callSessionManager.drop(internalId: internalId, reason: .hangUp, debugLog: .single(nil))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1121,6 +1165,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
private let sharedAudioContext: SharedCallAudioContext?
|
private let sharedAudioContext: SharedCallAudioContext?
|
||||||
|
|
||||||
public let isConference: Bool
|
public let isConference: Bool
|
||||||
|
private let beginWithVideo: Bool
|
||||||
|
|
||||||
private let conferenceSourceId: CallSessionInternalId?
|
private let conferenceSourceId: CallSessionInternalId?
|
||||||
public var conferenceSource: CallSessionInternalId? {
|
public var conferenceSource: CallSessionInternalId? {
|
||||||
@ -1153,6 +1198,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
keyPair: TelegramKeyPair?,
|
keyPair: TelegramKeyPair?,
|
||||||
conferenceSourceId: CallSessionInternalId?,
|
conferenceSourceId: CallSessionInternalId?,
|
||||||
isConference: Bool,
|
isConference: Bool,
|
||||||
|
beginWithVideo: Bool,
|
||||||
sharedAudioContext: SharedCallAudioContext?
|
sharedAudioContext: SharedCallAudioContext?
|
||||||
) {
|
) {
|
||||||
self.account = accountContext.account
|
self.account = accountContext.account
|
||||||
@ -1183,6 +1229,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
self.isStream = isStream
|
self.isStream = isStream
|
||||||
self.conferenceSourceId = conferenceSourceId
|
self.conferenceSourceId = conferenceSourceId
|
||||||
self.isConference = isConference
|
self.isConference = isConference
|
||||||
|
self.beginWithVideo = beginWithVideo
|
||||||
self.keyPair = keyPair
|
self.keyPair = keyPair
|
||||||
|
|
||||||
if let keyPair, let initialCall {
|
if let keyPair, let initialCall {
|
||||||
@ -1490,6 +1537,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
strongSelf.screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: basePath)
|
strongSelf.screencastBufferClientContext = IpcGroupCallBufferBroadcastContext(basePath: basePath)
|
||||||
})*/
|
})*/
|
||||||
|
|
||||||
|
if beginWithVideo {
|
||||||
|
self.requestVideo()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -3844,22 +3895,17 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO:release
|
if self.conferenceInvitationContexts[peerId] != nil {
|
||||||
let _ = self.accountContext.engine.calls.inviteConferenceCallParticipant(callId: initialCall.description.id, accessHash: initialCall.description.accessHash, peerId: peerId, isVideo: isVideo).start()
|
|
||||||
return false
|
|
||||||
/*guard let initialCall = self.initialCall else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if conferenceInvitationContexts[peerId] != nil {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var onStateUpdated: ((PendingConferenceInvitationContext.State) -> Void)?
|
var onStateUpdated: ((PendingConferenceInvitationContext.State) -> Void)?
|
||||||
var onEnded: ((Bool) -> Void)?
|
var onEnded: ((Bool) -> Void)?
|
||||||
var didEndAlready = false
|
var didEndAlready = false
|
||||||
let invitationContext = PendingConferenceInvitationContext(
|
let invitationContext = PendingConferenceInvitationContext(
|
||||||
callSessionManager: self.accountContext.account.callSessionManager,
|
engine: self.accountContext.engine,
|
||||||
groupCall: GroupCallReference(id: initialCall.id, accessHash: initialCall.accessHash),
|
reference: initialCall.reference,
|
||||||
peerId: peerId,
|
peerId: peerId,
|
||||||
|
isVideo: isVideo,
|
||||||
onStateUpdated: { state in
|
onStateUpdated: { state in
|
||||||
onStateUpdated?(state)
|
onStateUpdated?(state)
|
||||||
},
|
},
|
||||||
@ -3906,7 +3952,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false*/
|
return false
|
||||||
} else {
|
} else {
|
||||||
guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(where: { $0.id == peerId }) else {
|
guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(where: { $0.id == peerId }) else {
|
||||||
return false
|
return false
|
||||||
@ -3933,6 +3979,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
var updatedInvitedPeers = self.invitedPeersValue
|
var updatedInvitedPeers = self.invitedPeersValue
|
||||||
updatedInvitedPeers.removeAll(where: { $0.id == peerId})
|
updatedInvitedPeers.removeAll(where: { $0.id == peerId})
|
||||||
self.invitedPeersValue = updatedInvitedPeers
|
self.invitedPeersValue = updatedInvitedPeers
|
||||||
|
|
||||||
|
if let conferenceInvitationContext = self.conferenceInvitationContexts[peerId] {
|
||||||
|
self.conferenceInvitationContexts.removeValue(forKey: peerId)
|
||||||
|
if let messageId = conferenceInvitationContext.messageId {
|
||||||
|
self.accountContext.engine.account.callSessionManager.dropOutgoingConferenceRequest(messageId: messageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateTitle(_ title: String) {
|
public func updateTitle(_ title: String) {
|
||||||
|
|||||||
@ -5,6 +5,179 @@ import ComponentFlow
|
|||||||
import MultilineTextComponent
|
import MultilineTextComponent
|
||||||
import BalancedTextComponent
|
import BalancedTextComponent
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
import CallsEmoji
|
||||||
|
|
||||||
|
private final class EmojiItemComponent: Component {
|
||||||
|
let emoji: String?
|
||||||
|
|
||||||
|
init(emoji: String?) {
|
||||||
|
self.emoji = emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: EmojiItemComponent, rhs: EmojiItemComponent) -> Bool {
|
||||||
|
if lhs.emoji != rhs.emoji {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let measureEmojiView = ComponentView<Empty>()
|
||||||
|
private var pendingContainerView: UIView?
|
||||||
|
private var pendingEmojiViews: [ComponentView<Empty>] = []
|
||||||
|
private var emojiView: ComponentView<Empty>?
|
||||||
|
|
||||||
|
private var component: EmojiItemComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
private var pendingEmojiValues: [String]?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: EmojiItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
let size = self.measureEmojiView.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: "👍", font: Font.regular(40.0), textColor: .white))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: 200.0, height: 200.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let borderEmoji = 2
|
||||||
|
let numEmoji = borderEmoji * 2 + 3
|
||||||
|
|
||||||
|
if let emoji = component.emoji {
|
||||||
|
let emojiView: ComponentView<Empty>
|
||||||
|
var emojiViewTransition = transition
|
||||||
|
if let current = self.emojiView {
|
||||||
|
emojiView = current
|
||||||
|
} else {
|
||||||
|
emojiViewTransition = .immediate
|
||||||
|
emojiView = ComponentView()
|
||||||
|
self.emojiView = emojiView
|
||||||
|
}
|
||||||
|
let emojiSize = emojiView.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: emoji, font: Font.regular(40.0), textColor: .white))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: 200.0, height: 200.0)
|
||||||
|
)
|
||||||
|
let emojiFrame = CGRect(origin: CGPoint(x: floor((size.width - emojiSize.width) * 0.5), y: floor((size.height - emojiSize.height) * 0.5)), size: emojiSize)
|
||||||
|
if let emojiComponentView = emojiView.view {
|
||||||
|
if emojiComponentView.superview == nil {
|
||||||
|
self.addSubview(emojiComponentView)
|
||||||
|
}
|
||||||
|
emojiViewTransition.setFrame(view: emojiComponentView, frame: emojiFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pendingEmojiValues = nil
|
||||||
|
} else {
|
||||||
|
if let emojiView = self.emojiView {
|
||||||
|
self.emojiView = nil
|
||||||
|
emojiView.view?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.pendingEmojiValues?.count != numEmoji {
|
||||||
|
var pendingEmojiValuesValue: [String] = []
|
||||||
|
for _ in 0 ..< numEmoji - borderEmoji - 1 {
|
||||||
|
pendingEmojiValuesValue.append(randomCallsEmoji() ?? "👍")
|
||||||
|
}
|
||||||
|
for i in 0 ..< borderEmoji + 1 {
|
||||||
|
pendingEmojiValuesValue.append(pendingEmojiValuesValue[i])
|
||||||
|
}
|
||||||
|
self.pendingEmojiValues = pendingEmojiValuesValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let pendingEmojiValues, pendingEmojiValues.count == numEmoji {
|
||||||
|
let pendingContainerView: UIView
|
||||||
|
if let current = self.pendingContainerView {
|
||||||
|
pendingContainerView = current
|
||||||
|
} else {
|
||||||
|
pendingContainerView = UIView()
|
||||||
|
self.pendingContainerView = pendingContainerView
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0 ..< numEmoji {
|
||||||
|
let pendingEmojiView: ComponentView<Empty>
|
||||||
|
if self.pendingEmojiViews.count > i {
|
||||||
|
pendingEmojiView = self.pendingEmojiViews[i]
|
||||||
|
} else {
|
||||||
|
pendingEmojiView = ComponentView()
|
||||||
|
self.pendingEmojiViews.append(pendingEmojiView)
|
||||||
|
}
|
||||||
|
let pendingEmojiViewSize = pendingEmojiView.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: pendingEmojiValues[i], font: Font.regular(40.0), textColor: .white))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: 200.0, height: 200.0)
|
||||||
|
)
|
||||||
|
if let pendingEmojiComponentView = pendingEmojiView.view {
|
||||||
|
if pendingEmojiComponentView.superview == nil {
|
||||||
|
pendingContainerView.addSubview(pendingEmojiComponentView)
|
||||||
|
}
|
||||||
|
pendingEmojiComponentView.frame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(i) * size.height), size: pendingEmojiViewSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingContainerView.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
|
||||||
|
if pendingContainerView.superview == nil {
|
||||||
|
self.addSubview(pendingContainerView)
|
||||||
|
|
||||||
|
let animation = CABasicAnimation(keyPath: "sublayerTransform.translation.y")
|
||||||
|
//animation.duration = 4.2
|
||||||
|
animation.duration = 0.2
|
||||||
|
animation.fromValue = -CGFloat(numEmoji - borderEmoji) * size.height
|
||||||
|
animation.toValue = CGFloat(borderEmoji - 3) * size.height
|
||||||
|
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||||
|
animation.autoreverses = false
|
||||||
|
animation.repeatCount = .infinity
|
||||||
|
|
||||||
|
pendingContainerView.layer.add(animation, forKey: "offsetCycle")
|
||||||
|
}
|
||||||
|
} else if let pendingContainerView = self.pendingContainerView {
|
||||||
|
self.pendingContainerView = nil
|
||||||
|
pendingContainerView.removeFromSuperview()
|
||||||
|
|
||||||
|
for emojiView in self.pendingEmojiViews {
|
||||||
|
emojiView.view?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
self.pendingEmojiViews.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
//self.layer.borderColor = UIColor.red.cgColor
|
||||||
|
//self.layer.borderWidth = 4.0
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class VideoChatEncryptionKeyComponent: Component {
|
final class VideoChatEncryptionKeyComponent: Component {
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
@ -119,7 +292,7 @@ final class VideoChatEncryptionKeyComponent: Component {
|
|||||||
let expandedButtonTopInset: CGFloat = 12.0
|
let expandedButtonTopInset: CGFloat = 12.0
|
||||||
let expandedButtonBottomInset: CGFloat = 13.0
|
let expandedButtonBottomInset: CGFloat = 13.0
|
||||||
|
|
||||||
let emojiItemSizes = (0 ..< component.emoji.count).map { i -> CGSize in
|
let emojiItemSizes = (0 ..< 4).map { i -> CGSize in
|
||||||
let emojiItem: ComponentView<Empty>
|
let emojiItem: ComponentView<Empty>
|
||||||
if self.emojiItems.count > i {
|
if self.emojiItems.count > i {
|
||||||
emojiItem = self.emojiItems[i]
|
emojiItem = self.emojiItems[i]
|
||||||
@ -128,9 +301,9 @@ final class VideoChatEncryptionKeyComponent: Component {
|
|||||||
self.emojiItems.append(emojiItem)
|
self.emojiItems.append(emojiItem)
|
||||||
}
|
}
|
||||||
return emojiItem.update(
|
return emojiItem.update(
|
||||||
transition: .immediate,
|
transition: transition,
|
||||||
component: AnyComponent(MultilineTextComponent(
|
component: AnyComponent(EmojiItemComponent(
|
||||||
text: .plain(NSAttributedString(string: component.emoji[i], font: Font.regular(40.0), textColor: .white))
|
emoji: i < component.emoji.count ? component.emoji[i] : nil
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: 200.0, height: 200.0)
|
containerSize: CGSize(width: 200.0, height: 200.0)
|
||||||
|
|||||||
@ -7,16 +7,24 @@ import TelegramPresentationData
|
|||||||
import BundleIconComponent
|
import BundleIconComponent
|
||||||
|
|
||||||
final class VideoChatListInviteComponent: Component {
|
final class VideoChatListInviteComponent: Component {
|
||||||
|
enum Icon {
|
||||||
|
case addUser
|
||||||
|
case link
|
||||||
|
}
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
|
let icon: Icon
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
title: String,
|
title: String,
|
||||||
|
icon: Icon,
|
||||||
theme: PresentationTheme,
|
theme: PresentationTheme,
|
||||||
action: @escaping () -> Void
|
action: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
|
self.icon = icon
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.action = action
|
self.action = action
|
||||||
}
|
}
|
||||||
@ -25,6 +33,9 @@ final class VideoChatListInviteComponent: Component {
|
|||||||
if lhs.title != rhs.title {
|
if lhs.title != rhs.title {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.icon != rhs.icon {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.theme !== rhs.theme {
|
if lhs.theme !== rhs.theme {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -116,10 +127,17 @@ final class VideoChatListInviteComponent: Component {
|
|||||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let iconName: String
|
||||||
|
switch component.icon {
|
||||||
|
case .addUser:
|
||||||
|
iconName = "Chat/Context Menu/AddUser"
|
||||||
|
case .link:
|
||||||
|
iconName = "Chat/Context Menu/Link"
|
||||||
|
}
|
||||||
let iconSize = self.icon.update(
|
let iconSize = self.icon.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(BundleIconComponent(
|
component: AnyComponent(BundleIconComponent(
|
||||||
name: "Chat/Context Menu/AddUser",
|
name: iconName,
|
||||||
tintColor: component.theme.list.itemAccentColor
|
tintColor: component.theme.list.itemAccentColor
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
|
|||||||
@ -39,23 +39,33 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class Participants: Equatable {
|
final class Participants: Equatable {
|
||||||
enum InviteType {
|
enum InviteType: Equatable {
|
||||||
case invite
|
case invite(isMultipleUsers: Bool)
|
||||||
case shareLink
|
case shareLink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct InviteOption: Equatable {
|
||||||
|
let id: Int
|
||||||
|
let type: InviteType
|
||||||
|
|
||||||
|
init(id: Int, type: InviteType) {
|
||||||
|
self.id = id
|
||||||
|
self.type = type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let myPeerId: EnginePeer.Id
|
let myPeerId: EnginePeer.Id
|
||||||
let participants: [GroupCallParticipantsContext.Participant]
|
let participants: [GroupCallParticipantsContext.Participant]
|
||||||
let totalCount: Int
|
let totalCount: Int
|
||||||
let loadMoreToken: String?
|
let loadMoreToken: String?
|
||||||
let inviteType: InviteType?
|
let inviteOptions: [InviteOption]
|
||||||
|
|
||||||
init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteType: InviteType?) {
|
init(myPeerId: EnginePeer.Id, participants: [GroupCallParticipantsContext.Participant], totalCount: Int, loadMoreToken: String?, inviteOptions: [InviteOption]) {
|
||||||
self.myPeerId = myPeerId
|
self.myPeerId = myPeerId
|
||||||
self.participants = participants
|
self.participants = participants
|
||||||
self.totalCount = totalCount
|
self.totalCount = totalCount
|
||||||
self.loadMoreToken = loadMoreToken
|
self.loadMoreToken = loadMoreToken
|
||||||
self.inviteType = inviteType
|
self.inviteOptions = inviteOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: Participants, rhs: Participants) -> Bool {
|
static func ==(lhs: Participants, rhs: Participants) -> Bool {
|
||||||
@ -74,7 +84,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
if lhs.loadMoreToken != rhs.loadMoreToken {
|
if lhs.loadMoreToken != rhs.loadMoreToken {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if lhs.inviteType != rhs.inviteType {
|
if lhs.inviteOptions != rhs.inviteOptions {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -142,7 +152,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void
|
let updateMainParticipant: (VideoParticipantKey?, Bool?) -> Void
|
||||||
let updateIsMainParticipantPinned: (Bool) -> Void
|
let updateIsMainParticipantPinned: (Bool) -> Void
|
||||||
let updateIsExpandedUIHidden: (Bool) -> Void
|
let updateIsExpandedUIHidden: (Bool) -> Void
|
||||||
let openInviteMembers: () -> Void
|
let openInviteMembers: (Participants.InviteType) -> Void
|
||||||
let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void
|
let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@ -162,7 +172,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
|
updateMainParticipant: @escaping (VideoParticipantKey?, Bool?) -> Void,
|
||||||
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
|
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
|
||||||
updateIsExpandedUIHidden: @escaping (Bool) -> Void,
|
updateIsExpandedUIHidden: @escaping (Bool) -> Void,
|
||||||
openInviteMembers: @escaping () -> Void,
|
openInviteMembers: @escaping (Participants.InviteType) -> Void,
|
||||||
visibleParticipantsUpdated: @escaping (Set<EnginePeer.Id>) -> Void
|
visibleParticipantsUpdated: @escaping (Set<EnginePeer.Id>) -> Void
|
||||||
) {
|
) {
|
||||||
self.call = call
|
self.call = call
|
||||||
@ -379,14 +389,14 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
let sideInset: CGFloat
|
let sideInset: CGFloat
|
||||||
let itemCount: Int
|
let itemCount: Int
|
||||||
let itemHeight: CGFloat
|
let itemHeight: CGFloat
|
||||||
let trailingItemHeight: CGFloat
|
let trailingItemHeights: [CGFloat]
|
||||||
|
|
||||||
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeight: CGFloat) {
|
init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, itemHeight: CGFloat, trailingItemHeights: [CGFloat]) {
|
||||||
self.containerSize = containerSize
|
self.containerSize = containerSize
|
||||||
self.sideInset = sideInset
|
self.sideInset = sideInset
|
||||||
self.itemCount = itemCount
|
self.itemCount = itemCount
|
||||||
self.itemHeight = itemHeight
|
self.itemHeight = itemHeight
|
||||||
self.trailingItemHeight = trailingItemHeight
|
self.trailingItemHeights = trailingItemHeights
|
||||||
}
|
}
|
||||||
|
|
||||||
func frame(at index: Int) -> CGRect {
|
func frame(at index: Int) -> CGRect {
|
||||||
@ -394,8 +404,15 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
return frame
|
return frame
|
||||||
}
|
}
|
||||||
|
|
||||||
func trailingItemFrame() -> CGRect {
|
func trailingItemFrame(index: Int) -> CGRect {
|
||||||
return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeight))
|
if index < 0 || index >= self.trailingItemHeights.count {
|
||||||
|
return CGRect()
|
||||||
|
}
|
||||||
|
var prefixHeight: CGFloat = 0.0
|
||||||
|
for i in 0 ..< index {
|
||||||
|
prefixHeight += self.trailingItemHeights[i]
|
||||||
|
}
|
||||||
|
return CGRect(origin: CGPoint(x: self.sideInset, y: CGFloat(self.itemCount) * self.itemHeight + prefixHeight), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.trailingItemHeights[index]))
|
||||||
}
|
}
|
||||||
|
|
||||||
func contentHeight() -> CGFloat {
|
func contentHeight() -> CGFloat {
|
||||||
@ -403,7 +420,9 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
if self.itemCount != 0 {
|
if self.itemCount != 0 {
|
||||||
result = self.frame(at: self.itemCount - 1).maxY
|
result = self.frame(at: self.itemCount - 1).maxY
|
||||||
}
|
}
|
||||||
result += self.trailingItemHeight
|
for height in self.trailingItemHeights {
|
||||||
|
result += height
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,7 +458,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
let scrollClippingFrame: CGRect
|
let scrollClippingFrame: CGRect
|
||||||
let separateVideoScrollClippingFrame: CGRect
|
let separateVideoScrollClippingFrame: CGRect
|
||||||
|
|
||||||
init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
|
init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeights: [CGFloat]) {
|
||||||
self.containerSize = containerSize
|
self.containerSize = containerSize
|
||||||
self.layout = layout
|
self.layout = layout
|
||||||
self.isUIHidden = isUIHidden
|
self.isUIHidden = isUIHidden
|
||||||
@ -465,7 +484,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.grid = Grid(containerSize: CGSize(width: gridWidth, height: gridContainerHeight), sideInset: gridSideInset, itemCount: gridItemCount, isDedicatedColumn: layout.videoColumn != nil)
|
self.grid = Grid(containerSize: CGSize(width: gridWidth, height: gridContainerHeight), sideInset: gridSideInset, itemCount: gridItemCount, isDedicatedColumn: layout.videoColumn != nil)
|
||||||
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
|
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeights: listTrailingItemHeights)
|
||||||
self.spacing = 4.0
|
self.spacing = 4.0
|
||||||
|
|
||||||
if let videoColumn = layout.videoColumn, !isUIHidden && !layout.isMainColumnHidden {
|
if let videoColumn = layout.videoColumn, !isUIHidden && !layout.isMainColumnHidden {
|
||||||
@ -568,8 +587,8 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listTrailingItemFrame() -> CGRect {
|
func listTrailingItemFrame(index: Int) -> CGRect {
|
||||||
return self.list.trailingItemFrame()
|
return self.list.trailingItemFrame(index: index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -641,7 +660,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
private var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
private var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||||
|
|
||||||
private let measureListItemView = ComponentView<Empty>()
|
private let measureListItemView = ComponentView<Empty>()
|
||||||
private let inviteListItemView = ComponentView<Empty>()
|
private var inviteListItemViews: [Int: ComponentView<Empty>] = [:]
|
||||||
|
|
||||||
private var gridItemViews: [VideoParticipantKey: GridItem] = [:]
|
private var gridItemViews: [VideoParticipantKey: GridItem] = [:]
|
||||||
private let gridItemViewContainer: UIView
|
private let gridItemViewContainer: UIView
|
||||||
@ -1270,7 +1289,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
case .requesting:
|
case .requesting:
|
||||||
subtitle = PeerListItemComponent.Subtitle(text: "requesting...", color: .neutral)
|
subtitle = PeerListItemComponent.Subtitle(text: "requesting...", color: .neutral)
|
||||||
case .ringing:
|
case .ringing:
|
||||||
subtitle = PeerListItemComponent.Subtitle(text: "ringing...", color: .neutral)
|
subtitle = PeerListItemComponent.Subtitle(text: "invited", color: .neutral)
|
||||||
}
|
}
|
||||||
|
|
||||||
peerItemComponent = PeerListItemComponent(
|
peerItemComponent = PeerListItemComponent(
|
||||||
@ -1381,11 +1400,15 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
self.listItemViews.removeValue(forKey: itemId)
|
self.listItemViews.removeValue(forKey: itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
var trailingItemIndex = 0
|
||||||
|
for inviteOption in component.participants?.inviteOptions ?? [] {
|
||||||
|
guard let itemView = self.inviteListItemViews[inviteOption.id] else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
var itemTransition = transition
|
var itemTransition = transition
|
||||||
let itemView = self.inviteListItemView
|
|
||||||
|
|
||||||
let itemFrame = itemLayout.listTrailingItemFrame()
|
let itemFrame = itemLayout.listTrailingItemFrame(index: trailingItemIndex)
|
||||||
|
trailingItemIndex += 1
|
||||||
|
|
||||||
if let itemComponentView = itemView.view {
|
if let itemComponentView = itemView.view {
|
||||||
if itemComponentView.superview == nil {
|
if itemComponentView.superview == nil {
|
||||||
@ -1395,6 +1418,17 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var removeInviteListItemIds: [Int] = []
|
||||||
|
for (id, itemView) in self.inviteListItemViews {
|
||||||
|
if let participants = component.participants, participants.inviteOptions.contains(where: { $0.id == id }) {
|
||||||
|
} else {
|
||||||
|
removeInviteListItemIds.append(id)
|
||||||
|
itemView.view?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in removeInviteListItemIds {
|
||||||
|
self.inviteListItemViews.removeValue(forKey: id)
|
||||||
|
}
|
||||||
|
|
||||||
transition.setScale(view: self.gridItemViewContainer, scale: gridIsEmpty ? 0.001 : 1.0)
|
transition.setScale(view: self.gridItemViewContainer, scale: gridIsEmpty ? 0.001 : 1.0)
|
||||||
transition.setPosition(view: self.gridItemViewContainer, position: CGPoint(x: itemLayout.gridItemContainerFrame().midX, y: itemLayout.gridItemContainerFrame().minY))
|
transition.setPosition(view: self.gridItemViewContainer, position: CGPoint(x: itemLayout.gridItemContainerFrame().midX, y: itemLayout.gridItemContainerFrame().minY))
|
||||||
@ -1748,32 +1782,51 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var inviteListItemSizes: [CGSize] = []
|
||||||
|
for (inviteOption) in component.participants?.inviteOptions ?? [] {
|
||||||
let inviteText: String
|
let inviteText: String
|
||||||
if let participants = component.participants, let inviteType = participants.inviteType {
|
let iconType: VideoChatListInviteComponent.Icon
|
||||||
switch inviteType {
|
switch inviteOption.type {
|
||||||
case .invite:
|
case let .invite(isMultiple):
|
||||||
|
//TODO:localize
|
||||||
|
if isMultiple {
|
||||||
inviteText = component.strings.VoiceChat_InviteMember
|
inviteText = component.strings.VoiceChat_InviteMember
|
||||||
|
} else {
|
||||||
|
inviteText = "Add Member"
|
||||||
|
}
|
||||||
|
iconType = .addUser
|
||||||
case .shareLink:
|
case .shareLink:
|
||||||
inviteText = component.strings.VoiceChat_Share
|
inviteText = component.strings.VoiceChat_Share
|
||||||
|
iconType = .link
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let inviteListItemView: ComponentView<Empty>
|
||||||
|
var inviteListItemTransition = transition
|
||||||
|
if let current = self.inviteListItemViews[inviteOption.id] {
|
||||||
|
inviteListItemView = current
|
||||||
} else {
|
} else {
|
||||||
inviteText = component.strings.VoiceChat_InviteMember
|
inviteListItemView = ComponentView()
|
||||||
|
self.inviteListItemViews[inviteOption.id] = inviteListItemView
|
||||||
|
inviteListItemTransition = inviteListItemTransition.withAnimation(.none)
|
||||||
}
|
}
|
||||||
let inviteListItemSize = self.inviteListItemView.update(
|
|
||||||
transition: transition,
|
inviteListItemSizes.append(inviteListItemView.update(
|
||||||
|
transition: inviteListItemTransition,
|
||||||
component: AnyComponent(VideoChatListInviteComponent(
|
component: AnyComponent(VideoChatListInviteComponent(
|
||||||
title: inviteText,
|
title: inviteText,
|
||||||
|
icon: iconType,
|
||||||
theme: component.theme,
|
theme: component.theme,
|
||||||
action: { [weak self] in
|
action: { [weak self] in
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
component.openInviteMembers()
|
component.openInviteMembers(inviteOption.type)
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||||
)
|
))
|
||||||
|
}
|
||||||
|
|
||||||
var gridParticipants: [VideoParticipant] = []
|
var gridParticipants: [VideoParticipant] = []
|
||||||
var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
var listParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||||
@ -1824,7 +1877,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
gridItemCount: gridParticipants.count,
|
gridItemCount: gridParticipants.count,
|
||||||
listItemCount: listParticipants.count + component.invitedPeers.count,
|
listItemCount: listParticipants.count + component.invitedPeers.count,
|
||||||
listItemHeight: measureListItemSize.height,
|
listItemHeight: measureListItemSize.height,
|
||||||
listTrailingItemHeight: inviteListItemSize.height
|
listTrailingItemHeights: inviteListItemSizes.map(\.height)
|
||||||
)
|
)
|
||||||
self.itemLayout = itemLayout
|
self.itemLayout = itemLayout
|
||||||
|
|
||||||
|
|||||||
@ -1759,12 +1759,19 @@ final class VideoChatScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var inviteType: VideoChatParticipantsComponent.Participants.InviteType?
|
var inviteOptions: [VideoChatParticipantsComponent.Participants.InviteOption] = []
|
||||||
|
if case let .group(groupCall) = self.currentCall, groupCall.isConference {
|
||||||
|
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: .invite(isMultipleUsers: false)))
|
||||||
|
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 1, type: .shareLink))
|
||||||
|
} else {
|
||||||
if canInvite {
|
if canInvite {
|
||||||
|
let inviteType: VideoChatParticipantsComponent.Participants.InviteType
|
||||||
if inviteIsLink {
|
if inviteIsLink {
|
||||||
inviteType = .shareLink
|
inviteType = .shareLink
|
||||||
} else {
|
} else {
|
||||||
inviteType = .invite
|
inviteType = .invite(isMultipleUsers: false)
|
||||||
|
}
|
||||||
|
inviteOptions.append(VideoChatParticipantsComponent.Participants.InviteOption(id: 0, type: inviteType))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1773,7 +1780,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
participants: members.participants,
|
participants: members.participants,
|
||||||
totalCount: members.totalCount,
|
totalCount: members.totalCount,
|
||||||
loadMoreToken: members.loadMoreToken,
|
loadMoreToken: members.loadMoreToken,
|
||||||
inviteType: inviteType
|
inviteOptions: inviteOptions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2038,7 +2045,13 @@ final class VideoChatScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var encryptionKeyFrame: CGRect?
|
var encryptionKeyFrame: CGRect?
|
||||||
if let encryptionKeyEmoji = self.encryptionKeyEmoji {
|
var isConference = false
|
||||||
|
if case let .group(groupCall) = self.currentCall {
|
||||||
|
isConference = groupCall.isConference
|
||||||
|
} else if case .conferenceSource = self.currentCall {
|
||||||
|
isConference = true
|
||||||
|
}
|
||||||
|
if isConference {
|
||||||
navigationHeight -= 2.0
|
navigationHeight -= 2.0
|
||||||
let encryptionKey: ComponentView<Empty>
|
let encryptionKey: ComponentView<Empty>
|
||||||
var encryptionKeyTransition = transition
|
var encryptionKeyTransition = transition
|
||||||
@ -2055,7 +2068,7 @@ final class VideoChatScreenComponent: Component {
|
|||||||
component: AnyComponent(VideoChatEncryptionKeyComponent(
|
component: AnyComponent(VideoChatEncryptionKeyComponent(
|
||||||
theme: environment.theme,
|
theme: environment.theme,
|
||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
emoji: encryptionKeyEmoji,
|
emoji: self.encryptionKeyEmoji ?? [],
|
||||||
isExpanded: self.isEncryptionKeyExpanded,
|
isExpanded: self.isEncryptionKeyExpanded,
|
||||||
tapAction: { [weak self] in
|
tapAction: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -2326,11 +2339,18 @@ final class VideoChatScreenComponent: Component {
|
|||||||
self.state?.updated(transition: .spring(duration: 0.4))
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openInviteMembers: { [weak self] in
|
openInviteMembers: { [weak self] type in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if case .shareLink = type {
|
||||||
|
guard let inviteLinks = self.inviteLinks else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.presentShare(inviteLinks)
|
||||||
|
} else {
|
||||||
self.openInviteMembers()
|
self.openInviteMembers()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
visibleParticipantsUpdated: { [weak self] visibleParticipants in
|
visibleParticipantsUpdated: { [weak self] visibleParticipants in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
|
|||||||
@ -51,7 +51,15 @@ extension VideoChatScreenComponent.View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in
|
let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, shareLink: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let inviteLinks = self.inviteLinks else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.presentShare(inviteLinks)
|
||||||
|
}, completion: { [weak self] peerIds in
|
||||||
guard let self, case let .group(groupCall) = self.currentCall else {
|
guard let self, case let .group(groupCall) = self.currentCall else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -80,7 +88,7 @@ extension VideoChatScreenComponent.View {
|
|||||||
if inviteIsLink {
|
if inviteIsLink {
|
||||||
inviteType = .shareLink
|
inviteType = .shareLink
|
||||||
} else {
|
} else {
|
||||||
inviteType = .invite
|
inviteType = .invite(isMultipleUsers: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -718,6 +718,23 @@ private final class CallSessionManagerContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dropOutgoingConferenceRequest(messageId: MessageId) {
|
||||||
|
let addUpdates = self.addUpdates
|
||||||
|
let rejectSignal = self.network.request(Api.functions.phone.declineConferenceCallInvite(msgId: messageId.id))
|
||||||
|
|> map(Optional.init)
|
||||||
|
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|> mapToSignal { updates -> Signal<Never, NoError> in
|
||||||
|
if let updates {
|
||||||
|
addUpdates(updates)
|
||||||
|
}
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rejectConferenceInvitationDisposables.add(rejectSignal.startStrict())
|
||||||
|
}
|
||||||
|
|
||||||
func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal<String?, NoError>) {
|
func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal<String?, NoError>) {
|
||||||
for (id, context) in self.incomingConferenceInvitationContexts {
|
for (id, context) in self.incomingConferenceInvitationContexts {
|
||||||
if context.internalId == internalId {
|
if context.internalId == internalId {
|
||||||
@ -1383,6 +1400,12 @@ public final class CallSessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func dropOutgoingConferenceRequest(messageId: MessageId) {
|
||||||
|
self.withContext { context in
|
||||||
|
context.dropOutgoingConferenceRequest(messageId: messageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func drop(stableId: CallSessionStableId, reason: DropCallReason) {
|
func drop(stableId: CallSessionStableId, reason: DropCallReason) {
|
||||||
self.withContext { context in
|
self.withContext { context in
|
||||||
context.drop(stableId: stableId, reason: reason)
|
context.drop(stableId: stableId, reason: reason)
|
||||||
|
|||||||
@ -831,11 +831,11 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64, accessHash: Int64, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<Never, NoError> {
|
func _internal_inviteConferenceCallParticipant(account: Account, reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<MessageId?, NoError> {
|
||||||
return account.postbox.transaction { transaction -> Api.InputUser? in
|
return account.postbox.transaction { transaction -> Api.InputUser? in
|
||||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||||
}
|
}
|
||||||
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
|
|> mapToSignal { inputPeer -> Signal<MessageId?, NoError> in
|
||||||
guard let inputPeer else {
|
guard let inputPeer else {
|
||||||
return .complete()
|
return .complete()
|
||||||
}
|
}
|
||||||
@ -844,16 +844,19 @@ func _internal_inviteConferenceCallParticipant(account: Account, callId: Int64,
|
|||||||
if isVideo {
|
if isVideo {
|
||||||
flags |= 1 << 0
|
flags |= 1 << 0
|
||||||
}
|
}
|
||||||
return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(flags: flags, call: .inputGroupCall(id: callId, accessHash: accessHash), userId: inputPeer))
|
return account.network.request(Api.functions.phone.inviteConferenceCallParticipant(flags: flags, call: reference.apiInputGroupCall, userId: inputPeer))
|
||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||||
return .single(nil)
|
return .single(nil)
|
||||||
}
|
}
|
||||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
|> mapToSignal { result -> Signal<MessageId?, NoError> in
|
||||||
if let result {
|
if let result {
|
||||||
account.stateManager.addUpdates(result)
|
account.stateManager.addUpdates(result)
|
||||||
|
if let message = result.messageIds.first {
|
||||||
|
return .single(message)
|
||||||
}
|
}
|
||||||
return .complete()
|
}
|
||||||
|
return .single(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -105,8 +105,8 @@ public extension TelegramEngine {
|
|||||||
return _internal_sendConferenceCallBroadcast(account: self.account, callId: callId, accessHash: accessHash, block: block)
|
return _internal_sendConferenceCallBroadcast(account: self.account, callId: callId, accessHash: accessHash, block: block)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func inviteConferenceCallParticipant(callId: Int64, accessHash: Int64, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<Never, NoError> {
|
public func inviteConferenceCallParticipant(reference: InternalGroupCallReference, peerId: EnginePeer.Id, isVideo: Bool) -> Signal<EngineMessage.Id?, NoError> {
|
||||||
return _internal_inviteConferenceCallParticipant(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId, isVideo: isVideo)
|
return _internal_inviteConferenceCallParticipant(account: self.account, reference: reference, peerId: peerId, isVideo: isVideo)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal<RemoveGroupCallBlockchainParticipantsResult, NoError> {
|
public func removeGroupCallBlockchainParticipants(callId: Int64, accessHash: Int64, participantIds: [Int64], block: Data) -> Signal<RemoveGroupCallBlockchainParticipantsResult, NoError> {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ swift_library(
|
|||||||
"-warnings-as-errors",
|
"-warnings-as-errors",
|
||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit",
|
||||||
"//submodules/AsyncDisplayKit",
|
"//submodules/AsyncDisplayKit",
|
||||||
"//submodules/Display",
|
"//submodules/Display",
|
||||||
"//submodules/TelegramCore",
|
"//submodules/TelegramCore",
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import AppBundle
|
|||||||
import ChatMessageBubbleContentNode
|
import ChatMessageBubbleContentNode
|
||||||
import ChatMessageItemCommon
|
import ChatMessageItemCommon
|
||||||
import ChatMessageDateAndStatusNode
|
import ChatMessageDateAndStatusNode
|
||||||
|
import SwiftSignalKit
|
||||||
|
|
||||||
private let titleFont: UIFont = Font.medium(16.0)
|
private let titleFont: UIFont = Font.medium(16.0)
|
||||||
private let labelFont: UIFont = Font.regular(13.0)
|
private let labelFont: UIFont = Font.regular(13.0)
|
||||||
@ -25,6 +26,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
private let iconNode: ASImageNode
|
private let iconNode: ASImageNode
|
||||||
private let buttonNode: HighlightableButtonNode
|
private let buttonNode: HighlightableButtonNode
|
||||||
|
|
||||||
|
private var activeConferenceUpdateTimer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
required public init() {
|
required public init() {
|
||||||
self.titleNode = TextNode()
|
self.titleNode = TextNode()
|
||||||
self.labelNode = TextNode()
|
self.labelNode = TextNode()
|
||||||
@ -57,6 +60,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside)
|
self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.activeConferenceUpdateTimer?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
override public func accessibilityActivate() -> Bool {
|
override public func accessibilityActivate() -> Bool {
|
||||||
self.callButtonPressed()
|
self.callButtonPressed()
|
||||||
return true
|
return true
|
||||||
@ -90,6 +97,8 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
var callDuration: Int32?
|
var callDuration: Int32?
|
||||||
var callSuccessful = true
|
var callSuccessful = true
|
||||||
var isVideo = false
|
var isVideo = false
|
||||||
|
var hasCallButton = true
|
||||||
|
var updateConferenceTimerEndTimeout: Int32?
|
||||||
for media in item.message.media {
|
for media in item.message.media {
|
||||||
if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action {
|
if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action {
|
||||||
isVideo = isVideoValue
|
isVideo = isVideoValue
|
||||||
@ -124,10 +133,31 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
|
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
|
||||||
isVideo = false
|
isVideo = conferenceCall.flags.contains(.isVideo)
|
||||||
callDuration = conferenceCall.duration
|
callDuration = conferenceCall.duration
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
titleString = "Group Call"
|
let missedTimeout: Int32
|
||||||
|
#if DEBUG
|
||||||
|
missedTimeout = 5
|
||||||
|
#else
|
||||||
|
missedTimeout = 30
|
||||||
|
#endif
|
||||||
|
let currentTime = Int32(Date().timeIntervalSince1970)
|
||||||
|
if conferenceCall.flags.contains(.isMissed) {
|
||||||
|
titleString = "Declined Group Call"
|
||||||
|
} else if item.message.timestamp < currentTime - missedTimeout {
|
||||||
|
titleString = "Missed Group Call"
|
||||||
|
} else if conferenceCall.duration != nil {
|
||||||
|
titleString = "Cancelled Group Call"
|
||||||
|
hasCallButton = true
|
||||||
|
} else {
|
||||||
|
if incoming {
|
||||||
|
titleString = "Incoming Group Call"
|
||||||
|
} else {
|
||||||
|
titleString = "Outgoing Group Call"
|
||||||
|
}
|
||||||
|
updateConferenceTimerEndTimeout = (item.message.timestamp + missedTimeout) - currentTime
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,7 +241,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
||||||
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
|
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
|
||||||
|
|
||||||
|
if hasCallButton {
|
||||||
boundingSize.width += 54.0
|
boundingSize.width += 54.0
|
||||||
|
}
|
||||||
|
|
||||||
return (boundingSize.width, { boundingWidth in
|
return (boundingSize.width, { boundingWidth in
|
||||||
return (boundingSize, { [weak self] animation, _, _ in
|
return (boundingSize, { [weak self] animation, _, _ in
|
||||||
@ -234,6 +266,22 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
if let buttonImage = buttonImage {
|
if let buttonImage = buttonImage {
|
||||||
strongSelf.buttonNode.setImage(buttonImage, for: [])
|
strongSelf.buttonNode.setImage(buttonImage, for: [])
|
||||||
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: boundingWidth - buttonImage.size.width - 8.0, y: 15.0), size: buttonImage.size)
|
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: boundingWidth - buttonImage.size.width - 8.0, y: 15.0), size: buttonImage.size)
|
||||||
|
strongSelf.buttonNode.isHidden = !hasCallButton
|
||||||
|
}
|
||||||
|
|
||||||
|
if let activeConferenceUpdateTimer = strongSelf.activeConferenceUpdateTimer {
|
||||||
|
activeConferenceUpdateTimer.invalidate()
|
||||||
|
strongSelf.activeConferenceUpdateTimer = nil
|
||||||
|
}
|
||||||
|
if let updateConferenceTimerEndTimeout, updateConferenceTimerEndTimeout >= 0 {
|
||||||
|
strongSelf.activeConferenceUpdateTimer?.invalidate()
|
||||||
|
strongSelf.activeConferenceUpdateTimer = SwiftSignalKit.Timer(timeout: Double(updateConferenceTimerEndTimeout) + 0.5, repeat: false, completion: { [weak strongSelf] in
|
||||||
|
guard let strongSelf else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.requestInlineUpdate?()
|
||||||
|
}, queue: .mainQueue())
|
||||||
|
strongSelf.activeConferenceUpdateTimer?.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -270,6 +318,10 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||||
|
if self.buttonNode.isHidden {
|
||||||
|
return ChatMessageBubbleContentTapAction(content: .none)
|
||||||
|
}
|
||||||
|
|
||||||
if self.buttonNode.frame.contains(point) {
|
if self.buttonNode.frame.contains(point) {
|
||||||
return ChatMessageBubbleContentTapAction(content: .ignore)
|
return ChatMessageBubbleContentTapAction(content: .ignore)
|
||||||
} else if self.bounds.contains(point), let item = self.item {
|
} else if self.bounds.contains(point), let item = self.item {
|
||||||
|
|||||||
@ -405,7 +405,7 @@ private final class JoinSubjectScreenComponent: Component {
|
|||||||
isStream: false
|
isStream: false
|
||||||
),
|
),
|
||||||
reference: .link(slug: groupCall.slug),
|
reference: .link(slug: groupCall.slug),
|
||||||
mode: .joining
|
beginWithVideo: false
|
||||||
)
|
)
|
||||||
|
|
||||||
self.environment?.controller()?.dismiss()
|
self.environment?.controller()?.dismiss()
|
||||||
|
|||||||
@ -2908,7 +2908,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
isStream: false
|
isStream: false
|
||||||
),
|
),
|
||||||
reference: .message(id: message.id),
|
reference: .message(id: message.id),
|
||||||
mode: .joining
|
beginWithVideo: conferenceCall.flags.contains(.isVideo)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}, longTap: { [weak self] action, params in
|
}, longTap: { [weak self] action, params in
|
||||||
|
|||||||
@ -60,6 +60,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
|
private let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
|
||||||
|
private let isPeerEnabled: (ContactListPeer) -> Bool
|
||||||
var dismissed: (() -> Void)?
|
var dismissed: (() -> Void)?
|
||||||
|
|
||||||
var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void = { _ in }
|
var presentScheduleTimePicker: (@escaping (Int32) -> Void) -> Void = { _ in }
|
||||||
@ -107,6 +108,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
|||||||
self.displayDeviceContacts = params.displayDeviceContacts
|
self.displayDeviceContacts = params.displayDeviceContacts
|
||||||
self.displayCallIcons = params.displayCallIcons
|
self.displayCallIcons = params.displayCallIcons
|
||||||
self.confirmation = params.confirmation
|
self.confirmation = params.confirmation
|
||||||
|
self.isPeerEnabled = params.isPeerEnabled
|
||||||
self.multipleSelection = params.multipleSelection
|
self.multipleSelection = params.multipleSelection
|
||||||
self.requirePhoneNumbers = params.requirePhoneNumbers
|
self.requirePhoneNumbers = params.requirePhoneNumbers
|
||||||
self.allowChannelsInSearch = params.allowChannelsInSearch
|
self.allowChannelsInSearch = params.allowChannelsInSearch
|
||||||
@ -218,7 +220,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func loadDisplayNode() {
|
override func loadDisplayNode() {
|
||||||
self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers, allowChannelsInSearch: self.allowChannelsInSearch)
|
self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers, allowChannelsInSearch: self.allowChannelsInSearch, isPeerEnabled: self.isPeerEnabled)
|
||||||
self._ready.set(self.contactsNode.contactListNode.ready)
|
self._ready.set(self.contactsNode.contactListNode.ready)
|
||||||
|
|
||||||
self.contactsNode.navigationBar = self.navigationBar
|
self.contactsNode.navigationBar = self.navigationBar
|
||||||
|
|||||||
@ -44,6 +44,8 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
|||||||
var cancelSearch: (() -> Void)?
|
var cancelSearch: (() -> Void)?
|
||||||
var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)?
|
var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)?
|
||||||
|
|
||||||
|
let isPeerEnabled: (ContactListPeer) -> Bool
|
||||||
|
|
||||||
var presentationData: PresentationData {
|
var presentationData: PresentationData {
|
||||||
didSet {
|
didSet {
|
||||||
self.presentationDataPromise.set(.single(self.presentationData))
|
self.presentationDataPromise.set(.single(self.presentationData))
|
||||||
@ -57,12 +59,13 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
|||||||
|
|
||||||
var searchContainerNode: ContactsSearchContainerNode?
|
var searchContainerNode: ContactsSearchContainerNode?
|
||||||
|
|
||||||
init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool) {
|
init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool, allowChannelsInSearch: Bool, isPeerEnabled: @escaping (ContactListPeer) -> Bool) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.displayDeviceContacts = displayDeviceContacts
|
self.displayDeviceContacts = displayDeviceContacts
|
||||||
self.displayCallIcons = displayCallIcons
|
self.displayCallIcons = displayCallIcons
|
||||||
self.allowChannelsInSearch = allowChannelsInSearch
|
self.allowChannelsInSearch = allowChannelsInSearch
|
||||||
|
self.isPeerEnabled = isPeerEnabled
|
||||||
|
|
||||||
var excludeSelf = true
|
var excludeSelf = true
|
||||||
|
|
||||||
@ -124,7 +127,9 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
|
var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
|
||||||
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
|
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, isPeerEnabled: { peer in
|
||||||
|
return isPeerEnabled(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil))
|
||||||
|
}, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
|
||||||
contextActionImpl?(peer, node, gesture, nil)
|
contextActionImpl?(peer, node, gesture, nil)
|
||||||
} : nil, multipleSelection: multipleSelection)
|
} : nil, multipleSelection: multipleSelection)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user