mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Conference
This commit is contained in:
parent
6f5a0c405c
commit
869d607c4a
@ -483,12 +483,25 @@ public final class CallController: ViewController {
|
||||
}
|
||||
|
||||
private func conferenceAddParticipant() {
|
||||
var disablePeerIds: [EnginePeer.Id] = []
|
||||
disablePeerIds.append(self.call.context.account.peerId)
|
||||
disablePeerIds.append(self.call.peerId)
|
||||
let controller = CallController.openConferenceAddParticipant(context: self.call.context, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = self.call.upgradeToConference(invitePeerIds: peerIds, completion: { _ in
|
||||
})
|
||||
})
|
||||
self.push(controller)
|
||||
}
|
||||
|
||||
static func openConferenceAddParticipant(context: AccountContext, disablePeerIds: [EnginePeer.Id], completion: @escaping ([EnginePeer.Id]) -> Void) -> ViewController {
|
||||
//TODO:localize
|
||||
let context = self.call.context
|
||||
let callPeerId = self.call.peerId
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
|
||||
let controller = self.call.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(
|
||||
context: self.call.context,
|
||||
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(
|
||||
context: context,
|
||||
updatedPresentationData: (initial: presentationData, signal: .single(presentationData)),
|
||||
title: "Invite Members",
|
||||
mode: .peerSelection(searchChatList: true, searchGroups: false, searchChannels: false),
|
||||
@ -496,7 +509,7 @@ public final class CallController: ViewController {
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
if user.id == context.account.peerId || user.id == callPeerId {
|
||||
if disablePeerIds.contains(user.id) {
|
||||
return false
|
||||
}
|
||||
if user.botInfo != nil {
|
||||
@ -506,11 +519,7 @@ public final class CallController: ViewController {
|
||||
}
|
||||
))
|
||||
controller.navigationPresentation = .modal
|
||||
let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] result in
|
||||
guard let self else {
|
||||
controller?.dismiss()
|
||||
return
|
||||
}
|
||||
let _ = (controller.result |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in
|
||||
guard case let .result(peerIds, _) = result else {
|
||||
controller?.dismiss()
|
||||
return
|
||||
@ -532,11 +541,10 @@ public final class CallController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.call.upgradeToConference(invitePeerIds: invitePeerIds, completion: { _ in
|
||||
})
|
||||
completion(invitePeerIds)
|
||||
})
|
||||
|
||||
self.push(controller)
|
||||
return controller
|
||||
}
|
||||
|
||||
@objc private func backPressed() {
|
||||
|
@ -177,6 +177,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
public let internalId: CallSessionInternalId
|
||||
public let peerId: EnginePeer.Id
|
||||
public let isOutgoing: Bool
|
||||
private let isIncomingConference: Bool
|
||||
public var isVideo: Bool
|
||||
public var isVideoPossible: Bool
|
||||
private let enableStunMarking: Bool
|
||||
@ -327,6 +328,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
internalId: CallSessionInternalId,
|
||||
peerId: EnginePeer.Id,
|
||||
isOutgoing: Bool,
|
||||
isIncomingConference: Bool,
|
||||
peer: EnginePeer?,
|
||||
proxyServer: ProxyServerSettings?,
|
||||
auxiliaryServers: [CallAuxiliaryServer],
|
||||
@ -361,6 +363,7 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.internalId = internalId
|
||||
self.peerId = peerId
|
||||
self.isOutgoing = isOutgoing
|
||||
self.isIncomingConference = isIncomingConference
|
||||
self.isVideo = initialState?.type == .video
|
||||
self.isVideoPossible = isVideoPossible
|
||||
self.enableStunMarking = enableStunMarking
|
||||
@ -805,6 +808,9 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
conferenceCall.upgradedConferenceCall = self
|
||||
|
||||
conferenceCall.setInvitedPeers(self.pendingInviteToConferencePeerIds)
|
||||
for peerId in self.pendingInviteToConferencePeerIds {
|
||||
let _ = conferenceCall.invitePeer(peerId)
|
||||
}
|
||||
|
||||
conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted)
|
||||
if let videoCapturer = self.videoCapturer {
|
||||
@ -1027,9 +1033,33 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.callKitIntegration?.dropCall(uuid: self.internalId)
|
||||
}
|
||||
}
|
||||
if let presentationState {
|
||||
self.statePromise.set(presentationState)
|
||||
self.updateTone(presentationState, callContextState: callContextState, previous: previous)
|
||||
|
||||
var isConference = false
|
||||
if case let .active(_, _, _, _, _, _, _, _, conferenceCall) = sessionState.state {
|
||||
isConference = conferenceCall != nil
|
||||
} else if case .switchedToConference = sessionState.state {
|
||||
isConference = true
|
||||
}
|
||||
if self.conferenceCallImpl != nil {
|
||||
isConference = true
|
||||
}
|
||||
if self.conferenceStateValue != nil {
|
||||
isConference = true
|
||||
}
|
||||
if self.isIncomingConference {
|
||||
isConference = true
|
||||
}
|
||||
|
||||
if isConference {
|
||||
if self.currentTone != nil {
|
||||
self.currentTone = nil
|
||||
self.sharedAudioContext?.audioDevice?.setTone(tone: nil)
|
||||
}
|
||||
} else {
|
||||
if let presentationState {
|
||||
self.statePromise.set(presentationState)
|
||||
self.updateTone(presentationState, callContextState: callContextState, previous: previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1161,11 +1191,15 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
present(c, a)
|
||||
}, openSettings: {
|
||||
openSettings()
|
||||
}, { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
}, { [weak strongSelf] value in
|
||||
guard let strongSelf else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
if strongSelf.isIncomingConference {
|
||||
strongSelf.conferenceStateValue = .preparing
|
||||
}
|
||||
|
||||
strongSelf.callSessionManager.accept(internalId: strongSelf.internalId)
|
||||
if !fromCallKitAction {
|
||||
strongSelf.callKitIntegration?.answerCall(uuid: strongSelf.internalId)
|
||||
@ -1175,6 +1209,10 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if strongSelf.isIncomingConference {
|
||||
strongSelf.conferenceStateValue = .preparing
|
||||
}
|
||||
|
||||
strongSelf.callSessionManager.accept(internalId: strongSelf.internalId)
|
||||
if !fromCallKitAction {
|
||||
strongSelf.callKitIntegration?.answerCall(uuid: strongSelf.internalId)
|
||||
@ -1316,23 +1354,12 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
public func upgradeToConference(invitePeerIds: [EnginePeer.Id], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable {
|
||||
if let conferenceCall = self.conferenceCall {
|
||||
completion(conferenceCall)
|
||||
|
||||
for peerId in invitePeerIds {
|
||||
let _ = self.requestAddToConference(peerId: peerId)
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
self.pendingInviteToConferencePeerIds = invitePeerIds
|
||||
let index = self.upgradedToConferenceCompletions.add({ [weak self] call in
|
||||
let index = self.upgradedToConferenceCompletions.add({ call in
|
||||
completion(call)
|
||||
|
||||
if let self {
|
||||
for peerId in invitePeerIds {
|
||||
let _ = self.requestAddToConference(peerId: peerId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.conferenceStateValue = .preparing
|
||||
@ -1348,32 +1375,6 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
}
|
||||
}
|
||||
|
||||
private func requestAddToConference(peerId: EnginePeer.Id) -> Disposable {
|
||||
var conferenceCall: (conference: GroupCallReference, encryptionKey: Data)?
|
||||
if let sessionState = self.sessionState {
|
||||
switch sessionState.state {
|
||||
case let .active(_, key, _, _, _, _, _, _, conferenceCallValue):
|
||||
if let conferenceCallValue {
|
||||
conferenceCall = (conferenceCallValue, key)
|
||||
}
|
||||
case let .switchedToConference(key, _, conferenceCallValue):
|
||||
conferenceCall = (conferenceCallValue, key)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
guard let conferenceCall else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
return (self.callSessionManager.request(peerId: peerId, isVideo: false, enableVideo: true, conferenceCall: conferenceCall)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] requestedInternalId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
})
|
||||
}
|
||||
|
||||
public func setCurrentAudioOutput(_ output: AudioSessionOutput) {
|
||||
if let sharedAudioContext = self.sharedAudioContext {
|
||||
sharedAudioContext.setCurrentAudioOutput(output)
|
||||
|
@ -325,6 +325,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
internalId: firstState.2.id,
|
||||
peerId: firstState.2.peerId,
|
||||
isOutgoing: false,
|
||||
isIncomingConference: firstState.2.isConference,
|
||||
peer: EnginePeer(firstState.1),
|
||||
proxyServer: strongSelf.proxyServer,
|
||||
auxiliaryServers: [],
|
||||
@ -572,6 +573,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
internalId: internalId,
|
||||
peerId: peerId,
|
||||
isOutgoing: true,
|
||||
isIncomingConference: false,
|
||||
peer: nil,
|
||||
proxyServer: strongSelf.proxyServer,
|
||||
auxiliaryServers: [],
|
||||
|
@ -600,6 +600,52 @@ private final class ScreencastEmbeddedIPCContext: ScreencastIPCContext {
|
||||
}
|
||||
}
|
||||
|
||||
private final class PendingConferenceInvitationContext {
|
||||
private let callSessionManager: CallSessionManager
|
||||
private var requestDisposable: Disposable?
|
||||
private var stateDisposable: Disposable?
|
||||
private var internalId: CallSessionInternalId?
|
||||
|
||||
private var didNotifyEnded: Bool = false
|
||||
|
||||
init(callSessionManager: CallSessionManager, groupCall: GroupCallReference, encryptionKey: Data, peerId: PeerId, onEnded: @escaping () -> Void) {
|
||||
self.callSessionManager = callSessionManager
|
||||
|
||||
self.requestDisposable = (callSessionManager.request(peerId: peerId, isVideo: false, enableVideo: true, conferenceCall: (groupCall, encryptionKey))
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] internalId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.internalId = internalId
|
||||
|
||||
self.stateDisposable = (self.callSessionManager.callState(internalId: internalId)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
switch state.state {
|
||||
case .dropping, .terminated:
|
||||
if !self.didNotifyEnded {
|
||||
self.didNotifyEnded = true
|
||||
onEnded()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.requestDisposable?.dispose()
|
||||
self.stateDisposable?.dispose()
|
||||
|
||||
if let internalId = self.internalId {
|
||||
self.callSessionManager.drop(internalId: internalId, reason: .hangUp, debugLog: .single(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
private enum InternalState {
|
||||
case requesting
|
||||
@ -1018,7 +1064,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|
||||
let debugLog = Promise<String?>()
|
||||
|
||||
weak var upgradedConferenceCall: PresentationCallImpl?
|
||||
public weak var upgradedConferenceCall: PresentationCallImpl?
|
||||
private var conferenceInvitationContexts: [PeerId: PendingConferenceInvitationContext] = [:]
|
||||
|
||||
init(
|
||||
accountContext: AccountContext,
|
||||
@ -1859,6 +1906,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|
||||
var encryptionKey: Data?
|
||||
encryptionKey = self.encryptionKey?.key
|
||||
encryptionKey = nil
|
||||
|
||||
let contextAudioSessionActive: Signal<Bool, NoError>
|
||||
if self.sharedAudioContext != nil {
|
||||
@ -3575,17 +3623,55 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
}
|
||||
|
||||
public func invitePeer(_ peerId: PeerId) -> Bool {
|
||||
guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(peerId) else {
|
||||
if self.isConference {
|
||||
guard let initialCall = self.initialCall, let encryptionKey = self.encryptionKey else {
|
||||
return false
|
||||
}
|
||||
if conferenceInvitationContexts[peerId] != nil {
|
||||
return false
|
||||
}
|
||||
var onEnded: (() -> Void)?
|
||||
var didEndAlready = false
|
||||
let invitationContext = PendingConferenceInvitationContext(
|
||||
callSessionManager: self.accountContext.account.callSessionManager,
|
||||
groupCall: GroupCallReference(id: initialCall.id, accessHash: initialCall.accessHash),
|
||||
encryptionKey: encryptionKey.key,
|
||||
peerId: peerId,
|
||||
onEnded: {
|
||||
didEndAlready = true
|
||||
onEnded?()
|
||||
}
|
||||
)
|
||||
if !didEndAlready {
|
||||
conferenceInvitationContexts[peerId] = invitationContext
|
||||
if !self.invitedPeersValue.contains(peerId) {
|
||||
self.invitedPeersValue.append(peerId)
|
||||
}
|
||||
onEnded = { [weak self, weak invitationContext] in
|
||||
guard let self, let invitationContext else {
|
||||
return
|
||||
}
|
||||
if self.conferenceInvitationContexts[peerId] === invitationContext {
|
||||
self.conferenceInvitationContexts.removeValue(forKey: peerId)
|
||||
self.invitedPeersValue.removeAll(where: { $0 == peerId })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} else {
|
||||
guard let callInfo = self.internalState.callInfo, !self.invitedPeersValue.contains(peerId) else {
|
||||
return false
|
||||
}
|
||||
|
||||
var updatedInvitedPeers = self.invitedPeersValue
|
||||
updatedInvitedPeers.insert(peerId, at: 0)
|
||||
self.invitedPeersValue = updatedInvitedPeers
|
||||
|
||||
let _ = self.accountContext.engine.calls.inviteToGroupCall(callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var updatedInvitedPeers = self.invitedPeersValue
|
||||
updatedInvitedPeers.insert(peerId, at: 0)
|
||||
self.invitedPeersValue = updatedInvitedPeers
|
||||
|
||||
let _ = self.accountContext.engine.calls.inviteToGroupCall(callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func setInvitedPeers(_ peerIds: [PeerId]) {
|
||||
|
@ -1609,15 +1609,19 @@ final class VideoChatScreenComponent: Component {
|
||||
if let members = self.members, let callState = self.callState {
|
||||
var canInvite = true
|
||||
var inviteIsLink = false
|
||||
if case let .channel(peer) = self.peer {
|
||||
if peer.flags.contains(.isGigagroup) {
|
||||
if peer.flags.contains(.isCreator) || peer.adminRights != nil {
|
||||
} else {
|
||||
canInvite = false
|
||||
if case let .group(groupCall) = self.currentCall, groupCall.isConference {
|
||||
canInvite = true
|
||||
} else {
|
||||
if case let .channel(peer) = self.peer {
|
||||
if peer.flags.contains(.isGigagroup) {
|
||||
if peer.flags.contains(.isCreator) || peer.adminRights != nil {
|
||||
} else {
|
||||
canInvite = false
|
||||
}
|
||||
}
|
||||
if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) {
|
||||
inviteIsLink = true
|
||||
}
|
||||
}
|
||||
if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) {
|
||||
inviteIsLink = true
|
||||
}
|
||||
}
|
||||
var inviteType: VideoChatParticipantsComponent.Participants.InviteType?
|
||||
|
@ -13,336 +13,358 @@ extension VideoChatScreenComponent.View {
|
||||
return
|
||||
}
|
||||
|
||||
var canInvite = true
|
||||
var inviteIsLink = false
|
||||
if case let .channel(peer) = self.peer {
|
||||
if peer.flags.contains(.isGigagroup) {
|
||||
if peer.flags.contains(.isCreator) || peer.adminRights != nil {
|
||||
} else {
|
||||
canInvite = false
|
||||
if groupCall.isConference {
|
||||
var disablePeerIds: [EnginePeer.Id] = []
|
||||
disablePeerIds.append(groupCall.accountContext.account.peerId)
|
||||
if let members = self.members {
|
||||
for participant in members.participants {
|
||||
if !disablePeerIds.contains(participant.peer.id) {
|
||||
disablePeerIds.append(participant.peer.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) {
|
||||
inviteIsLink = true
|
||||
}
|
||||
}
|
||||
var inviteType: VideoChatParticipantsComponent.Participants.InviteType?
|
||||
if canInvite {
|
||||
if inviteIsLink {
|
||||
inviteType = .shareLink
|
||||
} else {
|
||||
inviteType = .invite
|
||||
}
|
||||
}
|
||||
|
||||
guard let inviteType else {
|
||||
return
|
||||
}
|
||||
guard let peerId = groupCall.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
switch inviteType {
|
||||
case .invite:
|
||||
let groupPeer = groupCall.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
let _ = (groupPeer
|
||||
|> deliverOnMainQueue).start(next: { [weak self] groupPeer in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall, let groupPeer else {
|
||||
let controller = CallController.openConferenceAddParticipant(context: groupCall.accountContext, disablePeerIds: disablePeerIds, completion: { [weak self] peerIds in
|
||||
guard let self, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
let inviteLinks = self.inviteLinks
|
||||
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
var canInviteMembers = true
|
||||
if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) {
|
||||
canInviteMembers = false
|
||||
}
|
||||
if !canInviteMembers {
|
||||
if let inviteLinks {
|
||||
self.presentShare(inviteLinks)
|
||||
}
|
||||
return
|
||||
}
|
||||
for peerId in peerIds {
|
||||
let _ = groupCall.invitePeer(peerId)
|
||||
}
|
||||
|
||||
var filters: [ChannelMembersSearchFilter] = []
|
||||
if let members = self.members {
|
||||
filters.append(.disable(Array(members.participants.map { $0.peer.id })))
|
||||
}
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil {
|
||||
filters.append(.excludeNonMembers)
|
||||
}
|
||||
} else if case let .legacyGroup(groupPeer) = groupPeer {
|
||||
if groupPeer.hasBannedPermission(.banAddMembers) {
|
||||
filters.append(.excludeNonMembers)
|
||||
}
|
||||
}
|
||||
filters.append(.excludeBots)
|
||||
|
||||
var dismissController: (() -> Void)?
|
||||
let controller = ChannelMembersSearchController(context: groupCall.accountContext, peerId: groupPeer.id, forceTheme: environment.theme, mode: .inviteToCall, filters: filters, openPeer: { [weak self] peer, participant in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
guard let callState = self.callState else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
||||
if peer.id == callState.myPeerId {
|
||||
return
|
||||
}
|
||||
if let participant {
|
||||
dismissController?()
|
||||
|
||||
if groupCall.invitePeer(participant.peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
})
|
||||
self.environment?.controller()?.push(controller)
|
||||
} else {
|
||||
var canInvite = true
|
||||
var inviteIsLink = false
|
||||
if case let .channel(peer) = self.peer {
|
||||
if peer.flags.contains(.isGigagroup) {
|
||||
if peer.flags.contains(.isCreator) || peer.adminRights != nil {
|
||||
} else {
|
||||
if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) {
|
||||
let text = environment.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
|
||||
environment.controller()?.present(textAlertController(context: groupCall.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in
|
||||
dismissController?()
|
||||
|
||||
guard let self, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (enqueueMessages(account: groupCall.accountContext.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true })
|
||||
})
|
||||
})]), in: .window(.root))
|
||||
} else {
|
||||
let text: String
|
||||
if case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info {
|
||||
text = environment.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), groupPeer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
canInvite = false
|
||||
}
|
||||
}
|
||||
if case .broadcast = peer.info, !(peer.addressName?.isEmpty ?? true) {
|
||||
inviteIsLink = true
|
||||
}
|
||||
}
|
||||
var inviteType: VideoChatParticipantsComponent.Participants.InviteType?
|
||||
if canInvite {
|
||||
if inviteIsLink {
|
||||
inviteType = .shareLink
|
||||
} else {
|
||||
inviteType = .invite
|
||||
}
|
||||
}
|
||||
|
||||
guard let inviteType else {
|
||||
return
|
||||
}
|
||||
guard let peerId = groupCall.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
switch inviteType {
|
||||
case .invite:
|
||||
let groupPeer = groupCall.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
let _ = (groupPeer
|
||||
|> deliverOnMainQueue).start(next: { [weak self] groupPeer in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall, let groupPeer else {
|
||||
return
|
||||
}
|
||||
let inviteLinks = self.inviteLinks
|
||||
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
var canInviteMembers = true
|
||||
if case .broadcast = groupPeer.info, !(groupPeer.addressName?.isEmpty ?? true) {
|
||||
canInviteMembers = false
|
||||
}
|
||||
if !canInviteMembers {
|
||||
if let inviteLinks {
|
||||
self.presentShare(inviteLinks)
|
||||
}
|
||||
|
||||
environment.controller()?.present(textAlertController(context: groupCall.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { [weak self] in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
guard let selfController = environment.controller() else {
|
||||
return
|
||||
}
|
||||
let inviteDisposable = self.inviteDisposable
|
||||
var inviteSignal = groupCall.accountContext.peerChannelMemberCategoriesContextsManager.addMembers(engine: groupCall.accountContext.engine, peerId: groupPeer.id, memberIds: [peer.id])
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
selfController?.present(controller, in: .window(.root))
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
inviteSignal = inviteSignal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
cancelImpl = {
|
||||
inviteDisposable.set(nil)
|
||||
}
|
||||
|
||||
inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in
|
||||
dismissController?()
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
|
||||
let text: String
|
||||
switch error {
|
||||
case .limitExceeded:
|
||||
text = environment.strings.Channel_ErrorAddTooMuch
|
||||
case .tooMuchJoined:
|
||||
text = environment.strings.Invite_ChannelsTooMuch
|
||||
case .generic:
|
||||
text = environment.strings.Login_UnknownError
|
||||
case .restricted:
|
||||
text = environment.strings.Channel_ErrorAddBlocked
|
||||
case .notMutualContact:
|
||||
if case .broadcast = groupPeer.info {
|
||||
text = environment.strings.Channel_AddUserLeftError
|
||||
} else {
|
||||
text = environment.strings.GroupInfo_AddUserLeftError
|
||||
}
|
||||
case .botDoesntSupportGroups:
|
||||
text = environment.strings.Channel_BotDoesntSupportGroups
|
||||
case .tooMuchBots:
|
||||
text = environment.strings.Channel_TooMuchBots
|
||||
case .bot:
|
||||
text = environment.strings.Login_UnknownError
|
||||
case .kicked:
|
||||
text = environment.strings.Channel_AddUserKickedError
|
||||
}
|
||||
environment.controller()?.present(textAlertController(context: groupCall.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, completed: { [weak self] in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if groupCall.invitePeer(peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
}))
|
||||
} else if case let .legacyGroup(groupPeer) = groupPeer {
|
||||
guard let selfController = environment.controller() else {
|
||||
return
|
||||
}
|
||||
let inviteDisposable = self.inviteDisposable
|
||||
var inviteSignal = groupCall.accountContext.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id)
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
selfController?.present(controller, in: .window(.root))
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
inviteSignal = inviteSignal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
cancelImpl = {
|
||||
inviteDisposable.set(nil)
|
||||
}
|
||||
|
||||
inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in
|
||||
dismissController?()
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
let context = groupCall.accountContext
|
||||
|
||||
switch error {
|
||||
case .privacy:
|
||||
let _ = (groupCall.accountContext.account.postbox.loadedPeerWithId(peer.id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
environment.controller()?.present(textAlertController(context: groupCall.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
})
|
||||
case .notMutualContact:
|
||||
environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
case .tooManyChannels:
|
||||
environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
case .groupFull, .generic:
|
||||
environment.controller()?.present(textAlertController(context: context, forceTheme: environment.theme, title: nil, text: environment.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}, completed: { [weak self] in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if groupCall.invitePeer(peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
}))
|
||||
}
|
||||
})]), in: .window(.root))
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
controller.copyInviteLink = { [weak self] in
|
||||
dismissController?()
|
||||
|
||||
guard let self, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
var filters: [ChannelMembersSearchFilter] = []
|
||||
if let members = self.members {
|
||||
filters.append(.disable(Array(members.participants.map { $0.peer.id })))
|
||||
}
|
||||
guard let callPeerId = groupCall.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (groupCall.accountContext.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId),
|
||||
TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId)
|
||||
)
|
||||
|> map { peer, exportedInvitation -> String? in
|
||||
if let link = inviteLinks?.listenerLink {
|
||||
return link
|
||||
} else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty {
|
||||
return "https://t.me/\(addressName)"
|
||||
} else if let link = exportedInvitation?.link {
|
||||
return link
|
||||
} else {
|
||||
return nil
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
if !groupPeer.hasPermission(.inviteMembers) && inviteLinks?.listenerLink == nil {
|
||||
filters.append(.excludeNonMembers)
|
||||
}
|
||||
} else if case let .legacyGroup(groupPeer) = groupPeer {
|
||||
if groupPeer.hasBannedPermission(.banAddMembers) {
|
||||
filters.append(.excludeNonMembers)
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] link in
|
||||
guard let self, let environment = self.environment else {
|
||||
filters.append(.excludeBots)
|
||||
|
||||
var dismissController: (() -> Void)?
|
||||
let controller = ChannelMembersSearchController(context: groupCall.accountContext, peerId: groupPeer.id, forceTheme: environment.theme, mode: .inviteToCall, filters: filters, openPeer: { [weak self] peer, participant in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
guard let callState = self.callState else {
|
||||
return
|
||||
}
|
||||
|
||||
if let link {
|
||||
UIPasteboard.general.string = link
|
||||
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
||||
if peer.id == callState.myPeerId {
|
||||
return
|
||||
}
|
||||
if let participant {
|
||||
dismissController?()
|
||||
|
||||
self.presentUndoOverlay(content: .linkCopied(title: nil, text: environment.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false })
|
||||
if groupCall.invitePeer(participant.peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
} else {
|
||||
if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) {
|
||||
let text = environment.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
|
||||
environment.controller()?.present(textAlertController(context: groupCall.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in
|
||||
dismissController?()
|
||||
|
||||
guard let self, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (enqueueMessages(account: groupCall.accountContext.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true })
|
||||
})
|
||||
})]), in: .window(.root))
|
||||
} else {
|
||||
let text: String
|
||||
if case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info {
|
||||
text = environment.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), groupPeer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
|
||||
}
|
||||
|
||||
environment.controller()?.present(textAlertController(context: groupCall.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { [weak self] in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
|
||||
if case let .channel(groupPeer) = groupPeer {
|
||||
guard let selfController = environment.controller() else {
|
||||
return
|
||||
}
|
||||
let inviteDisposable = self.inviteDisposable
|
||||
var inviteSignal = groupCall.accountContext.peerChannelMemberCategoriesContextsManager.addMembers(engine: groupCall.accountContext.engine, peerId: groupPeer.id, memberIds: [peer.id])
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
selfController?.present(controller, in: .window(.root))
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
inviteSignal = inviteSignal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
cancelImpl = {
|
||||
inviteDisposable.set(nil)
|
||||
}
|
||||
|
||||
inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in
|
||||
dismissController?()
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
|
||||
let text: String
|
||||
switch error {
|
||||
case .limitExceeded:
|
||||
text = environment.strings.Channel_ErrorAddTooMuch
|
||||
case .tooMuchJoined:
|
||||
text = environment.strings.Invite_ChannelsTooMuch
|
||||
case .generic:
|
||||
text = environment.strings.Login_UnknownError
|
||||
case .restricted:
|
||||
text = environment.strings.Channel_ErrorAddBlocked
|
||||
case .notMutualContact:
|
||||
if case .broadcast = groupPeer.info {
|
||||
text = environment.strings.Channel_AddUserLeftError
|
||||
} else {
|
||||
text = environment.strings.GroupInfo_AddUserLeftError
|
||||
}
|
||||
case .botDoesntSupportGroups:
|
||||
text = environment.strings.Channel_BotDoesntSupportGroups
|
||||
case .tooMuchBots:
|
||||
text = environment.strings.Channel_TooMuchBots
|
||||
case .bot:
|
||||
text = environment.strings.Login_UnknownError
|
||||
case .kicked:
|
||||
text = environment.strings.Channel_AddUserKickedError
|
||||
}
|
||||
environment.controller()?.present(textAlertController(context: groupCall.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, completed: { [weak self] in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if groupCall.invitePeer(peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
}))
|
||||
} else if case let .legacyGroup(groupPeer) = groupPeer {
|
||||
guard let selfController = environment.controller() else {
|
||||
return
|
||||
}
|
||||
let inviteDisposable = self.inviteDisposable
|
||||
var inviteSignal = groupCall.accountContext.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id)
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
selfController?.present(controller, in: .window(.root))
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
inviteSignal = inviteSignal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
cancelImpl = {
|
||||
inviteDisposable.set(nil)
|
||||
}
|
||||
|
||||
inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in
|
||||
dismissController?()
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
let context = groupCall.accountContext
|
||||
|
||||
switch error {
|
||||
case .privacy:
|
||||
let _ = (groupCall.accountContext.account.postbox.loadedPeerWithId(peer.id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
environment.controller()?.present(textAlertController(context: groupCall.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
})
|
||||
case .notMutualContact:
|
||||
environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
case .tooManyChannels:
|
||||
environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
case .groupFull, .generic:
|
||||
environment.controller()?.present(textAlertController(context: context, forceTheme: environment.theme, title: nil, text: environment.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}, completed: { [weak self] in
|
||||
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
|
||||
dismissController?()
|
||||
return
|
||||
}
|
||||
dismissController?()
|
||||
|
||||
if groupCall.invitePeer(peer.id) {
|
||||
let text: String
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
} else {
|
||||
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
|
||||
}
|
||||
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
|
||||
}
|
||||
}))
|
||||
}
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
})
|
||||
controller.copyInviteLink = { [weak self] in
|
||||
dismissController?()
|
||||
|
||||
guard let self, case let .group(groupCall) = self.currentCall else {
|
||||
return
|
||||
}
|
||||
guard let callPeerId = groupCall.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (groupCall.accountContext.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId),
|
||||
TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId)
|
||||
)
|
||||
|> map { peer, exportedInvitation -> String? in
|
||||
if let link = inviteLinks?.listenerLink {
|
||||
return link
|
||||
} else if let peer = peer, let addressName = peer.addressName, !addressName.isEmpty {
|
||||
return "https://t.me/\(addressName)"
|
||||
} else if let link = exportedInvitation?.link {
|
||||
return link
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] link in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
|
||||
if let link {
|
||||
UIPasteboard.general.string = link
|
||||
|
||||
self.presentUndoOverlay(content: .linkCopied(title: nil, text: environment.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false })
|
||||
}
|
||||
})
|
||||
}
|
||||
dismissController = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
environment.controller()?.push(controller)
|
||||
})
|
||||
case .shareLink:
|
||||
guard let inviteLinks = self.inviteLinks else {
|
||||
return
|
||||
}
|
||||
dismissController = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
environment.controller()?.push(controller)
|
||||
})
|
||||
case .shareLink:
|
||||
guard let inviteLinks = self.inviteLinks else {
|
||||
return
|
||||
self.presentShare(inviteLinks)
|
||||
}
|
||||
self.presentShare(inviteLinks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -137,6 +137,7 @@ public struct CallSessionRingingState: Equatable {
|
||||
public let peerId: PeerId
|
||||
public let isVideo: Bool
|
||||
public let isVideoPossible: Bool
|
||||
public let isConference: Bool
|
||||
}
|
||||
|
||||
public enum DropCallReason {
|
||||
@ -334,6 +335,7 @@ private func parseConnectionSet(primary: Api.PhoneConnection, alternative: [Api.
|
||||
private final class CallSessionContext {
|
||||
let peerId: PeerId
|
||||
let isOutgoing: Bool
|
||||
let isConference: Bool
|
||||
var type: CallSession.CallType
|
||||
var isVideoPossible: Bool
|
||||
let pendingConference: (conference: GroupCallReference, encryptionKey: Data)?
|
||||
@ -353,9 +355,10 @@ private final class CallSessionContext {
|
||||
}
|
||||
}
|
||||
|
||||
init(peerId: PeerId, isOutgoing: Bool, type: CallSession.CallType, isVideoPossible: Bool, pendingConference: (conference: GroupCallReference, encryptionKey: Data)?, state: CallSessionInternalState) {
|
||||
init(peerId: PeerId, isOutgoing: Bool, isConference: Bool, type: CallSession.CallType, isVideoPossible: Bool, pendingConference: (conference: GroupCallReference, encryptionKey: Data)?, state: CallSessionInternalState) {
|
||||
self.peerId = peerId
|
||||
self.isOutgoing = isOutgoing
|
||||
self.isConference = isConference
|
||||
self.type = type
|
||||
self.isVideoPossible = isVideoPossible
|
||||
self.pendingConference = pendingConference
|
||||
@ -547,7 +550,13 @@ private final class CallSessionManagerContext {
|
||||
var ringingContexts: [CallSessionRingingState] = []
|
||||
for (id, context) in self.contexts {
|
||||
if case .ringing = context.state {
|
||||
ringingContexts.append(CallSessionRingingState(id: id, peerId: context.peerId, isVideo: context.type == .video, isVideoPossible: context.isVideoPossible))
|
||||
ringingContexts.append(CallSessionRingingState(
|
||||
id: id,
|
||||
peerId: context.peerId,
|
||||
isVideo: context.type == .video,
|
||||
isVideoPossible: context.isVideoPossible,
|
||||
isConference: context.isConference
|
||||
))
|
||||
}
|
||||
}
|
||||
return ringingContexts
|
||||
@ -590,7 +599,7 @@ private final class CallSessionManagerContext {
|
||||
//#endif
|
||||
|
||||
let internalId = CallSessionManager.getStableIncomingUUID(stableId: stableId)
|
||||
let context = CallSessionContext(peerId: peerId, isOutgoing: false, type: isVideo ? .video : .audio, isVideoPossible: isVideoPossible, pendingConference: nil, state: .ringing(id: stableId, accessHash: accessHash, gAHash: gAHash, b: b, versions: versions, conferenceCall: conferenceCall))
|
||||
let context = CallSessionContext(peerId: peerId, isOutgoing: false, isConference: conferenceCall != nil, type: isVideo ? .video : .audio, isVideoPossible: isVideoPossible, pendingConference: nil, state: .ringing(id: stableId, accessHash: accessHash, gAHash: gAHash, b: b, versions: versions, conferenceCall: conferenceCall))
|
||||
self.contexts[internalId] = context
|
||||
let queue = self.queue
|
||||
|
||||
@ -1164,7 +1173,7 @@ private final class CallSessionManagerContext {
|
||||
let randomStatus = SecRandomCopyBytes(nil, 256, aBytes.assumingMemoryBound(to: UInt8.self))
|
||||
let a = Data(bytesNoCopy: aBytes, count: 256, deallocator: .free)
|
||||
if randomStatus == 0 {
|
||||
self.contexts[internalId] = CallSessionContext(peerId: peerId, isOutgoing: true, type: isVideo ? .video : .audio, isVideoPossible: enableVideo || isVideo, pendingConference: conferenceCall, state: .requesting(a: a, conferenceCall: conferenceCall?.conference, disposable: (requestCallSession(postbox: self.postbox, network: self.network, peerId: peerId, a: a, maxLayer: self.maxLayer, versions: self.filteredVersions(enableVideo: true), isVideo: isVideo, conferenceCall: conferenceCall?.conference) |> deliverOn(queue)).start(next: { [weak self] result in
|
||||
self.contexts[internalId] = CallSessionContext(peerId: peerId, isOutgoing: true, isConference: conferenceCall != nil, type: isVideo ? .video : .audio, isVideoPossible: enableVideo || isVideo, pendingConference: conferenceCall, state: .requesting(a: a, conferenceCall: conferenceCall?.conference, disposable: (requestCallSession(postbox: self.postbox, network: self.network, peerId: peerId, a: a, maxLayer: self.maxLayer, versions: self.filteredVersions(enableVideo: true), isVideo: isVideo, conferenceCall: conferenceCall?.conference) |> deliverOn(queue)).start(next: { [weak self] result in
|
||||
if let strongSelf = self, let context = strongSelf.contexts[internalId] {
|
||||
if case .requesting = context.state {
|
||||
switch result {
|
||||
|
@ -91,7 +91,7 @@ final class ConferenceButtonView: HighlightTrackingButton, OverlayMaskContainerV
|
||||
transition.setFrame(view: self.backdropBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size))
|
||||
|
||||
if self.iconView.image == nil {
|
||||
self.iconView.image = UIImage(bundleImageName: "Contact List/AddMemberIcon")?.withRenderingMode(.alwaysTemplate)
|
||||
self.iconView.image = UIImage(bundleImageName: "Call/CallNavigationAddPerson")?.withRenderingMode(.alwaysTemplate)
|
||||
self.iconView.tintColor = .white
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ final class EmojiTooltipView: OverlayMaskContainerView {
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
let anchorPoint = CGPoint(x: self.bounds.width - 46.0, y: 0.0)
|
||||
let anchorPoint = CGPoint(x: self.bounds.width * 0.5, y: 0.0)
|
||||
|
||||
self.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
@ -75,7 +75,7 @@ final class EmojiTooltipView: OverlayMaskContainerView {
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
let anchorPoint = CGPoint(x: self.bounds.width - 46.0, y: 0.0)
|
||||
let anchorPoint = CGPoint(x: self.bounds.width * 0.5, y: 0.0)
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
@ -126,7 +126,7 @@ final class EmojiTooltipView: OverlayMaskContainerView {
|
||||
addRoundedRectPath(context: context, rect: CGRect(origin: CGPoint(x: 0.0, y: arrowHeight), size: CGSize(width: size.width, height: size.height - arrowHeight)), radius: 14.0)
|
||||
context.fillPath()
|
||||
|
||||
context.translateBy(x: size.width - floor(params.subjectWidth * 0.5) - 20.0, y: 0.0)
|
||||
context.translateBy(x: size.width * 0.5 - 10.0, y: 0.0)
|
||||
let _ = try? drawSvgPath(context, path: "M9.0981,1.1979 C9.547,0.6431 10.453,0.6431 10.9019,1.1979 C12.4041,3.0542 15.6848,6.5616 20,8 H-0.0002 C4.3151,6.5616 7.5959,3.0542 9.0981,1.1978 Z ")
|
||||
})
|
||||
self.backgroundView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
@ -93,7 +93,7 @@ final class KeyEmojiView: HighlightTrackingButton {
|
||||
for i in 0 ..< self.emojiViews.count {
|
||||
let emojiView = self.emojiViews[i]
|
||||
emojiView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
emojiView.layer.animatePosition(from: CGPoint(x: -CGFloat(self.emojiViews.count - 1 - i) * 30.0, y: 0.0), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
emojiView.layer.animatePosition(from: CGPoint(x: CGFloat(i) * 30.0, y: 0.0), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,7 +109,7 @@ final class KeyEmojiView: HighlightTrackingButton {
|
||||
}
|
||||
|
||||
private func update(params: Params, transition: ComponentTransition) -> CGSize {
|
||||
let itemSpacing: CGFloat = 3.0
|
||||
let itemSpacing: CGFloat = 1.0
|
||||
|
||||
var height: CGFloat = 0.0
|
||||
var nextX = 0.0
|
||||
|
@ -610,7 +610,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
if self.hideEmojiTooltipTimer == nil && !self.areControlsHidden {
|
||||
self.displayEmojiTooltip = true
|
||||
|
||||
self.hideEmojiTooltipTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false, block: { [weak self] _ in
|
||||
self.hideEmojiTooltipTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -962,126 +962,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
}
|
||||
}
|
||||
|
||||
if !isConferencePossible, case let .active(activeState) = params.state.lifecycleState {
|
||||
let emojiView: KeyEmojiView
|
||||
var emojiTransition = transition
|
||||
var emojiAlphaTransition = genericAlphaTransition
|
||||
if let current = self.emojiView {
|
||||
emojiView = current
|
||||
} else {
|
||||
emojiTransition = transition.withAnimation(.none)
|
||||
emojiAlphaTransition = genericAlphaTransition.withAnimation(.none)
|
||||
emojiView = KeyEmojiView(emoji: activeState.emojiKey)
|
||||
self.emojiView = emojiView
|
||||
emojiView.pressAction = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.isEmojiKeyExpanded {
|
||||
self.isEmojiKeyExpanded = true
|
||||
self.displayEmojiTooltip = false
|
||||
self.update(transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
}
|
||||
if emojiView.superview == nil {
|
||||
self.addSubview(emojiView)
|
||||
if !transition.animation.isImmediate {
|
||||
emojiView.animateIn()
|
||||
}
|
||||
}
|
||||
emojiView.isUserInteractionEnabled = !self.isEmojiKeyExpanded
|
||||
|
||||
let emojiViewWasExpanded = emojiView.isExpanded
|
||||
let emojiViewSize = emojiView.update(isExpanded: self.isEmojiKeyExpanded, transition: emojiTransition)
|
||||
|
||||
if self.isEmojiKeyExpanded {
|
||||
let emojiViewFrame = CGRect(origin: CGPoint(x: floor((params.size.width - emojiViewSize.width) * 0.5), y: params.insets.top + 93.0), size: emojiViewSize)
|
||||
|
||||
if case let .curve(duration, curve) = transition.animation, let emojiViewWasExpanded, !emojiViewWasExpanded {
|
||||
let distance = CGPoint(x: emojiViewFrame.midX - emojiView.center.x, y: emojiViewFrame.midY - emojiView.center.y)
|
||||
let positionKeyframes = generateParabollicMotionKeyframes(from: emojiView.center, to: emojiViewFrame.center, elevation: -distance.y * 0.8, duration: duration, curve: curve, reverse: false)
|
||||
emojiView.center = emojiViewFrame.center
|
||||
emojiView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position", additive: false)
|
||||
} else {
|
||||
emojiTransition.setPosition(view: emojiView, position: emojiViewFrame.center)
|
||||
}
|
||||
emojiTransition.setBounds(view: emojiView, bounds: CGRect(origin: CGPoint(), size: emojiViewFrame.size))
|
||||
if self.isAnimatedOutToGroupCall {
|
||||
emojiAlphaTransition.setAlpha(view: emojiView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
if let emojiTooltipView = self.emojiTooltipView {
|
||||
self.emojiTooltipView = nil
|
||||
emojiTooltipView.animateOut(completion: { [weak emojiTooltipView] in
|
||||
emojiTooltipView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let emojiY: CGFloat
|
||||
if currentAreControlsHidden {
|
||||
emojiY = -8.0 - emojiViewSize.height
|
||||
} else {
|
||||
emojiY = params.insets.top + 12.0
|
||||
}
|
||||
let emojiViewFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - 12.0 - emojiViewSize.width, y: emojiY), size: emojiViewSize)
|
||||
|
||||
if case let .curve(duration, curve) = transition.animation, let emojiViewWasExpanded, emojiViewWasExpanded {
|
||||
let distance = CGPoint(x: emojiViewFrame.midX - emojiView.center.x, y: emojiViewFrame.midY - emojiView.center.y)
|
||||
let positionKeyframes = generateParabollicMotionKeyframes(from: emojiViewFrame.center, to: emojiView.center, elevation: distance.y * 0.8, duration: duration, curve: curve, reverse: true)
|
||||
emojiView.center = emojiViewFrame.center
|
||||
emojiView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position", additive: false)
|
||||
} else {
|
||||
emojiTransition.setPosition(view: emojiView, position: emojiViewFrame.center)
|
||||
}
|
||||
emojiTransition.setBounds(view: emojiView, bounds: CGRect(origin: CGPoint(), size: emojiViewFrame.size))
|
||||
emojiAlphaTransition.setAlpha(view: emojiView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0)
|
||||
|
||||
if self.displayEmojiTooltip {
|
||||
let emojiTooltipView: EmojiTooltipView
|
||||
var emojiTooltipTransition = transition
|
||||
var animateIn = false
|
||||
if let current = self.emojiTooltipView {
|
||||
emojiTooltipView = current
|
||||
} else {
|
||||
emojiTooltipTransition = emojiTooltipTransition.withAnimation(.none)
|
||||
emojiTooltipView = EmojiTooltipView(text: params.state.strings.Call_EncryptionKeyTooltip)
|
||||
animateIn = true
|
||||
self.emojiTooltipView = emojiTooltipView
|
||||
self.addSubview(emojiTooltipView)
|
||||
}
|
||||
|
||||
let emojiTooltipSize = emojiTooltipView.update(constrainedWidth: params.size.width - 32.0 * 2.0, subjectWidth: emojiViewSize.width - 20.0)
|
||||
let emojiTooltipFrame = CGRect(origin: CGPoint(x: emojiViewFrame.maxX - emojiTooltipSize.width, y: emojiViewFrame.maxY + 8.0), size: emojiTooltipSize)
|
||||
emojiTooltipTransition.setFrame(view: emojiTooltipView, frame: emojiTooltipFrame)
|
||||
|
||||
if animateIn && !transition.animation.isImmediate {
|
||||
emojiTooltipView.animateIn()
|
||||
}
|
||||
} else if let emojiTooltipView = self.emojiTooltipView {
|
||||
self.emojiTooltipView = nil
|
||||
emojiTooltipView.animateOut(completion: { [weak emojiTooltipView] in
|
||||
emojiTooltipView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
emojiAlphaTransition.setAlpha(view: emojiView, alpha: 1.0)
|
||||
} else {
|
||||
if let emojiView = self.emojiView {
|
||||
self.emojiView = nil
|
||||
genericAlphaTransition.setAlpha(view: emojiView, alpha: 0.0, completion: { [weak emojiView] _ in
|
||||
emojiView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
if let emojiTooltipView = self.emojiTooltipView {
|
||||
self.emojiTooltipView = nil
|
||||
emojiTooltipView.animateOut(completion: { [weak emojiTooltipView] in
|
||||
emojiTooltipView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let collapsedAvatarSize: CGFloat = 136.0
|
||||
let blobSize: CGFloat = collapsedAvatarSize + 40.0
|
||||
|
||||
@ -1435,9 +1315,91 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
transition.setFrame(view: self.titleView, frame: titleFrame)
|
||||
genericAlphaTransition.setAlpha(view: self.titleView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0)
|
||||
|
||||
var emojiViewSizeValue: CGSize?
|
||||
var emojiTransition = transition
|
||||
var emojiAlphaTransition = genericAlphaTransition
|
||||
let emojiViewWasExpanded = self.emojiView?.isExpanded ?? false
|
||||
if case let .active(activeState) = params.state.lifecycleState {
|
||||
let emojiView: KeyEmojiView
|
||||
if let current = self.emojiView {
|
||||
emojiView = current
|
||||
} else {
|
||||
emojiTransition = transition.withAnimation(.none)
|
||||
emojiAlphaTransition = genericAlphaTransition.withAnimation(.none)
|
||||
emojiView = KeyEmojiView(emoji: activeState.emojiKey)
|
||||
self.emojiView = emojiView
|
||||
emojiView.pressAction = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.isEmojiKeyExpanded {
|
||||
self.isEmojiKeyExpanded = true
|
||||
self.displayEmojiTooltip = false
|
||||
self.update(transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
}
|
||||
if emojiView.superview == nil {
|
||||
self.addSubview(emojiView)
|
||||
if !transition.animation.isImmediate {
|
||||
emojiView.animateIn()
|
||||
}
|
||||
}
|
||||
emojiView.isUserInteractionEnabled = !self.isEmojiKeyExpanded
|
||||
|
||||
let emojiViewSize = emojiView.update(isExpanded: self.isEmojiKeyExpanded, transition: emojiTransition)
|
||||
emojiViewSizeValue = emojiViewSize
|
||||
|
||||
if self.isEmojiKeyExpanded {
|
||||
let emojiViewFrame = CGRect(origin: CGPoint(x: floor((params.size.width - emojiViewSize.width) * 0.5), y: params.insets.top + 93.0), size: emojiViewSize)
|
||||
|
||||
if case let .curve(duration, curve) = transition.animation, !emojiViewWasExpanded {
|
||||
let distance = CGPoint(x: emojiViewFrame.midX - emojiView.center.x, y: emojiViewFrame.midY - emojiView.center.y)
|
||||
let positionKeyframes = generateParabollicMotionKeyframes(from: emojiView.center, to: emojiViewFrame.center, elevation: -distance.y * 0.8, duration: duration, curve: curve, reverse: false)
|
||||
emojiView.center = emojiViewFrame.center
|
||||
emojiView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position", additive: false)
|
||||
} else {
|
||||
emojiTransition.setPosition(view: emojiView, position: emojiViewFrame.center)
|
||||
}
|
||||
emojiTransition.setBounds(view: emojiView, bounds: CGRect(origin: CGPoint(), size: emojiViewFrame.size))
|
||||
if self.isAnimatedOutToGroupCall {
|
||||
emojiAlphaTransition.setAlpha(view: emojiView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
if let emojiTooltipView = self.emojiTooltipView {
|
||||
self.emojiTooltipView = nil
|
||||
emojiTooltipView.animateOut(completion: { [weak emojiTooltipView] in
|
||||
emojiTooltipView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Inline mode handled after calculating status frame
|
||||
}
|
||||
|
||||
emojiAlphaTransition.setAlpha(view: emojiView, alpha: 1.0)
|
||||
} else {
|
||||
if let emojiView = self.emojiView {
|
||||
self.emojiView = nil
|
||||
genericAlphaTransition.setAlpha(view: emojiView, alpha: 0.0, completion: { [weak emojiView] _ in
|
||||
emojiView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
if let emojiTooltipView = self.emojiTooltipView {
|
||||
self.emojiTooltipView = nil
|
||||
emojiTooltipView.animateOut(completion: { [weak emojiTooltipView] in
|
||||
emojiTooltipView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var statusEmojiWidth: CGFloat = 0.0
|
||||
let statusEmojiSpacing: CGFloat = 8.0
|
||||
if !self.isEmojiKeyExpanded, let emojiViewSize = emojiViewSizeValue {
|
||||
statusEmojiWidth = statusEmojiSpacing + emojiViewSize.width
|
||||
}
|
||||
let statusFrame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: (params.size.width - statusSize.width) * 0.5,
|
||||
x: (params.size.width - statusSize.width - statusEmojiWidth) * 0.5,
|
||||
y: titleFrame.maxY + (havePrimaryVideo ? 0.0 : 4.0)
|
||||
),
|
||||
size: statusSize
|
||||
@ -1455,6 +1417,51 @@ public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictu
|
||||
genericAlphaTransition.setAlpha(view: self.statusView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
if case .active = params.state.lifecycleState {
|
||||
if let emojiView = self.emojiView, !self.isEmojiKeyExpanded, let emojiViewSize = emojiViewSizeValue {
|
||||
let emojiViewFrame = CGRect(origin: CGPoint(x: statusFrame.maxX + statusEmojiSpacing, y: statusFrame.minY + floorToScreenPixels((statusFrame.height - emojiViewSize.height) * 0.5)), size: emojiViewSize)
|
||||
|
||||
if case let .curve(duration, curve) = transition.animation, emojiViewWasExpanded {
|
||||
let distance = CGPoint(x: emojiViewFrame.midX - emojiView.center.x, y: emojiViewFrame.midY - emojiView.center.y)
|
||||
let positionKeyframes = generateParabollicMotionKeyframes(from: emojiViewFrame.center, to: emojiView.center, elevation: distance.y * 0.8, duration: duration, curve: curve, reverse: true)
|
||||
emojiView.center = emojiViewFrame.center
|
||||
emojiView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position", additive: false)
|
||||
} else {
|
||||
emojiTransition.setPosition(view: emojiView, position: emojiViewFrame.center)
|
||||
}
|
||||
emojiTransition.setBounds(view: emojiView, bounds: CGRect(origin: CGPoint(), size: emojiViewFrame.size))
|
||||
emojiAlphaTransition.setAlpha(view: emojiView, alpha: (currentAreControlsHidden || self.isAnimatedOutToGroupCall) ? 0.0 : 1.0)
|
||||
|
||||
if self.displayEmojiTooltip {
|
||||
let emojiTooltipView: EmojiTooltipView
|
||||
var emojiTooltipTransition = transition
|
||||
var animateIn = false
|
||||
if let current = self.emojiTooltipView {
|
||||
emojiTooltipView = current
|
||||
} else {
|
||||
emojiTooltipTransition = emojiTooltipTransition.withAnimation(.none)
|
||||
emojiTooltipView = EmojiTooltipView(text: params.state.strings.Call_EncryptionKeyTooltip)
|
||||
animateIn = true
|
||||
self.emojiTooltipView = emojiTooltipView
|
||||
self.addSubview(emojiTooltipView)
|
||||
}
|
||||
|
||||
let emojiTooltipSize = emojiTooltipView.update(constrainedWidth: params.size.width - 32.0 * 2.0, subjectWidth: emojiViewSize.width * 0.5)
|
||||
let emojiTooltipFrame = CGRect(origin: CGPoint(x: emojiViewFrame.minX + floor((emojiViewFrame.width - emojiTooltipSize.width) * 0.5), y: emojiViewFrame.maxY + 8.0), size: emojiTooltipSize)
|
||||
emojiTooltipTransition.setFrame(view: emojiTooltipView, frame: emojiTooltipFrame)
|
||||
|
||||
if animateIn && !transition.animation.isImmediate {
|
||||
emojiTooltipView.animateIn()
|
||||
}
|
||||
} else if let emojiTooltipView = self.emojiTooltipView {
|
||||
self.emojiTooltipView = nil
|
||||
emojiTooltipView.animateOut(completion: { [weak emojiTooltipView] in
|
||||
emojiTooltipView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case let .active(activeState) = params.state.lifecycleState, activeState.signalInfo.quality <= 0.2, !self.isEmojiKeyExpanded, (!self.displayEmojiTooltip || !havePrimaryVideo) {
|
||||
let weakSignalView: WeakSignalView
|
||||
if let current = self.weakSignalView {
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Call/CallNavigationAddPerson.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Call/CallNavigationAddPerson.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "addcaller.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Call/CallNavigationAddPerson.imageset/addcaller.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Call/CallNavigationAddPerson.imageset/addcaller.pdf
vendored
Normal file
Binary file not shown.
@ -807,6 +807,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}
|
||||
|
||||
if call !== self.call {
|
||||
let previousCall = self.call
|
||||
self.call = call
|
||||
|
||||
self.callController?.dismiss()
|
||||
@ -814,6 +815,25 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
self.hasOngoingCall.set(false)
|
||||
self.callState.set(.single(nil))
|
||||
|
||||
if let previousCall, let groupCallController = self.groupCallController {
|
||||
var matches = false
|
||||
switch groupCallController.call {
|
||||
case let .conferenceSource(conferenceSource):
|
||||
if conferenceSource === previousCall {
|
||||
matches = true
|
||||
}
|
||||
case let .group(groupCall):
|
||||
if (groupCall as? PresentationGroupCallImpl)?.upgradedConferenceCall === previousCall {
|
||||
matches = true
|
||||
}
|
||||
}
|
||||
|
||||
if matches {
|
||||
self.groupCallController = nil
|
||||
groupCallController.dismiss(closing: true, manual: false)
|
||||
}
|
||||
}
|
||||
|
||||
self.notificationController?.setBlocking(nil)
|
||||
|
||||
self.callPeerDisposable?.dispose()
|
||||
@ -822,7 +842,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
self.callIsConferenceDisposable = nil
|
||||
|
||||
if let call {
|
||||
if call.conferenceCall == nil {
|
||||
if call.conferenceStateValue == nil && call.conferenceCall == nil {
|
||||
self.callState.set(call.state
|
||||
|> map(Optional.init))
|
||||
}
|
||||
@ -841,6 +861,11 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return
|
||||
}
|
||||
guard let callController = self.callController, callController.call === call else {
|
||||
if self.callController == nil, call.conferenceStateValue != nil {
|
||||
self.callState.set(.single(nil))
|
||||
self.presentControllerWithCurrentCall()
|
||||
self.notificationController?.setBlocking(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
if call.conferenceStateValue != nil {
|
||||
|
@ -90,6 +90,7 @@ public:
|
||||
_audioDeviceModule->StopPlayout();
|
||||
_audioDeviceModule->StopRecording();
|
||||
}
|
||||
_audioDeviceModule->ActualTerminate();
|
||||
_audioDeviceModule = nullptr;
|
||||
} else {
|
||||
tgcalls::StaticThreads::getThreads()->getWorkerThread()->BlockingCall([&]() {
|
||||
@ -97,6 +98,7 @@ public:
|
||||
_audioDeviceModule->StopPlayout();
|
||||
_audioDeviceModule->StopRecording();
|
||||
}
|
||||
_audioDeviceModule->ActualTerminate();
|
||||
_audioDeviceModule = nullptr;
|
||||
});
|
||||
}
|
||||
|
2
third-party/webp/BUILD
vendored
2
third-party/webp/BUILD
vendored
@ -56,7 +56,7 @@ genrule(
|
||||
|
||||
mkdir -p "$$BUILD_DIR/Public/libwebp"
|
||||
|
||||
PATH="$$PATH:$$CMAKE_DIR/cmake-3.23.1-macos-universal/CMake.app/Contents/bin" sh $$BUILD_DIR/build-webp-bazel.sh $$BUILD_ARCH "$$BUILD_DIR/libwebp" "$$BUILD_DIR"
|
||||
PATH="$$PATH:$$CMAKE_DIR/cmake-3.23.1-macos-universal/CMake.app/Contents/bin" sh $$BUILD_DIR/build-webp-bazel.sh $$BUILD_ARCH "$$BUILD_DIR/libwebp" "$$BUILD_DIR" >/dev/null 2>&1
|
||||
""" +
|
||||
"\n".join([
|
||||
"cp -f \"$$BUILD_DIR/libwebp/src/webp/{}\" \"$(location Public/webp/{})\"".format(header, header) for header in headers
|
||||
|
Loading…
x
Reference in New Issue
Block a user