mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-16 19:30:29 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
0ec1dd0484
@ -41,6 +41,7 @@ swift_library(
|
|||||||
"//submodules/AnimatedCountLabelNode:AnimatedCountLabelNode",
|
"//submodules/AnimatedCountLabelNode:AnimatedCountLabelNode",
|
||||||
"//submodules/DeviceProximity:DeviceProximity",
|
"//submodules/DeviceProximity:DeviceProximity",
|
||||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||||
|
"//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import UniversalMediaPlayer
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import DeviceProximity
|
import DeviceProximity
|
||||||
import UndoUI
|
import UndoUI
|
||||||
|
import TemporaryCachedPeerDataManager
|
||||||
|
|
||||||
private extension GroupCallParticipantsContext.Participant {
|
private extension GroupCallParticipantsContext.Participant {
|
||||||
var allSsrcs: Set<UInt32> {
|
var allSsrcs: Set<UInt32> {
|
||||||
@ -353,10 +354,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
public let peerId: PeerId
|
public let peerId: PeerId
|
||||||
private let invite: String?
|
private let invite: String?
|
||||||
private var joinAsPeerId: PeerId
|
private var joinAsPeerId: PeerId
|
||||||
|
private var ignorePreviousJoinAsPeerId: (PeerId, UInt32)?
|
||||||
|
|
||||||
public private(set) var isVideo: Bool
|
public private(set) var isVideo: Bool
|
||||||
|
|
||||||
private let temporaryJoinTimestamp: Int32
|
private var temporaryJoinTimestamp: Int32
|
||||||
|
private var temporaryActivityTimestamp: Double?
|
||||||
|
private var temporaryActivityRank: Int?
|
||||||
|
|
||||||
private var internalState: InternalState = .requesting
|
private var internalState: InternalState = .requesting
|
||||||
private let internalStatePromise = Promise<InternalState>(.requesting)
|
private let internalStatePromise = Promise<InternalState>(.requesting)
|
||||||
@ -526,6 +530,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
private var processedMissingSsrcs = Set<UInt32>()
|
private var processedMissingSsrcs = Set<UInt32>()
|
||||||
private let missingSsrcsDisposable = MetaDisposable()
|
private let missingSsrcsDisposable = MetaDisposable()
|
||||||
private var isRequestingMissingSsrcs: Bool = false
|
private var isRequestingMissingSsrcs: Bool = false
|
||||||
|
|
||||||
|
private var peerUpdatesSubscription: Disposable?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
accountContext: AccountContext,
|
accountContext: AccountContext,
|
||||||
@ -738,11 +744,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in
|
if let initialCall = initialCall, let temporaryParticipantsContext = (self.accountContext.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl)?.impl.syncWith({ impl in
|
||||||
impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title))
|
impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash, title: initialCall.title))
|
||||||
}) {
|
}) {
|
||||||
if let participantsContext = temporaryParticipantsContext.context.participantsContext, let immediateState = participantsContext.immediateState {
|
self.switchToTemporaryParticipantsContext(sourceContext: temporaryParticipantsContext.context.participantsContext, oldMyPeerId: self.joinAsPeerId)
|
||||||
self.switchToTemporaryParticipantsContext(sourceContext: participantsContext, initialState: immediateState, oldMyPeerId: self.joinAsPeerId)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
self.updateSessionState(internalState: .requesting, audioSessionControl: nil)
|
self.switchToTemporaryParticipantsContext(sourceContext: nil, oldMyPeerId: self.joinAsPeerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.removedChannelMembersDisposable = (accountContext.peerChannelMemberCategoriesContextsManager.removedChannelMembers
|
self.removedChannelMembersDisposable = (accountContext.peerChannelMemberCategoriesContextsManager.removedChannelMembers
|
||||||
@ -759,6 +763,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
let _ = (self.account.postbox.loadedPeerWithId(peerId)
|
let _ = (self.account.postbox.loadedPeerWithId(peerId)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
var canManageCall = false
|
var canManageCall = false
|
||||||
if let peer = peer as? TelegramGroup {
|
if let peer = peer as? TelegramGroup {
|
||||||
if case .creator = peer.role {
|
if case .creator = peer.role {
|
||||||
@ -772,14 +779,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
} else if (peer.adminRights?.rights.contains(.canManageCalls) == true) {
|
} else if (peer.adminRights?.rights.contains(.canManageCalls) == true) {
|
||||||
canManageCall = true
|
canManageCall = true
|
||||||
}
|
}
|
||||||
|
strongSelf.peerUpdatesSubscription = strongSelf.accountContext.account.viewTracker.polledChannel(peerId: peer.id).start()
|
||||||
}
|
}
|
||||||
if let strongSelf = self {
|
var updatedValue = strongSelf.stateValue
|
||||||
var updatedValue = strongSelf.stateValue
|
updatedValue.canManageCall = canManageCall
|
||||||
updatedValue.canManageCall = canManageCall
|
strongSelf.stateValue = updatedValue
|
||||||
strongSelf.stateValue = updatedValue
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
self.requestCall(movingFromBroadcastToRtc: false)
|
self.requestCall(movingFromBroadcastToRtc: false)
|
||||||
}
|
}
|
||||||
@ -813,9 +818,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
self.audioOutputStateDisposable?.dispose()
|
self.audioOutputStateDisposable?.dispose()
|
||||||
|
|
||||||
self.removedChannelMembersDisposable?.dispose()
|
self.removedChannelMembersDisposable?.dispose()
|
||||||
|
|
||||||
|
self.peerUpdatesSubscription?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func switchToTemporaryParticipantsContext(sourceContext: GroupCallParticipantsContext, initialState: GroupCallParticipantsContext.State, oldMyPeerId: PeerId) {
|
private func switchToTemporaryParticipantsContext(sourceContext: GroupCallParticipantsContext?, oldMyPeerId: PeerId) {
|
||||||
let myPeerId = self.joinAsPeerId
|
let myPeerId = self.joinAsPeerId
|
||||||
let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in
|
let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in
|
||||||
if let peer = transaction.getPeer(myPeerId) {
|
if let peer = transaction.getPeer(myPeerId) {
|
||||||
@ -824,101 +831,166 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let temporaryParticipantsContext = GroupCallParticipantsContext(account: self.account, peerId: self.peerId, myPeerId: myPeerId, id: sourceContext.id, accessHash: sourceContext.accessHash, state: initialState)
|
if let sourceContext = sourceContext, let initialState = sourceContext.immediateState {
|
||||||
self.temporaryParticipantsContext = temporaryParticipantsContext
|
let temporaryParticipantsContext = GroupCallParticipantsContext(account: self.account, peerId: self.peerId, myPeerId: myPeerId, id: sourceContext.id, accessHash: sourceContext.accessHash, state: initialState)
|
||||||
self.participantsContextStateDisposable.set((combineLatest(queue: .mainQueue(),
|
self.temporaryParticipantsContext = temporaryParticipantsContext
|
||||||
myPeer,
|
self.participantsContextStateDisposable.set((combineLatest(queue: .mainQueue(),
|
||||||
temporaryParticipantsContext.state,
|
myPeer,
|
||||||
temporaryParticipantsContext.activeSpeakers
|
temporaryParticipantsContext.state,
|
||||||
)
|
temporaryParticipantsContext.activeSpeakers
|
||||||
|> take(1)).start(next: { [weak self] myPeerAndCachedData, state, activeSpeakers in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var topParticipants: [GroupCallParticipantsContext.Participant] = []
|
|
||||||
|
|
||||||
var members = PresentationGroupCallMembers(
|
|
||||||
participants: [],
|
|
||||||
speakingParticipants: [],
|
|
||||||
totalCount: 0,
|
|
||||||
loadMoreToken: nil
|
|
||||||
)
|
)
|
||||||
|
|> take(1)).start(next: { [weak self] myPeerAndCachedData, state, activeSpeakers in
|
||||||
var updatedInvitedPeers = strongSelf.invitedPeersValue
|
guard let strongSelf = self else {
|
||||||
var didUpdateInvitedPeers = false
|
return
|
||||||
|
}
|
||||||
var participants = state.participants
|
|
||||||
|
var topParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||||
if oldMyPeerId != myPeerId {
|
|
||||||
for i in 0 ..< participants.count {
|
var members = PresentationGroupCallMembers(
|
||||||
if participants[i].peer.id == oldMyPeerId {
|
participants: [],
|
||||||
participants.remove(at: i)
|
speakingParticipants: [],
|
||||||
break
|
totalCount: 0,
|
||||||
|
loadMoreToken: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
var updatedInvitedPeers = strongSelf.invitedPeersValue
|
||||||
|
var didUpdateInvitedPeers = false
|
||||||
|
|
||||||
|
var participants = state.participants
|
||||||
|
|
||||||
|
if oldMyPeerId != myPeerId {
|
||||||
|
for i in 0 ..< participants.count {
|
||||||
|
if participants[i].peer.id == oldMyPeerId {
|
||||||
|
participants.remove(at: i)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if !participants.contains(where: { $0.peer.id == myPeerId }) {
|
||||||
if !participants.contains(where: { $0.peer.id == myPeerId }) {
|
if let (myPeer, cachedData) = myPeerAndCachedData {
|
||||||
if let (myPeer, cachedData) = myPeerAndCachedData {
|
let about: String?
|
||||||
let about: String?
|
if let cachedData = cachedData as? CachedUserData {
|
||||||
if let cachedData = cachedData as? CachedUserData {
|
about = cachedData.about
|
||||||
about = cachedData.about
|
} else if let cachedData = cachedData as? CachedUserData {
|
||||||
} else if let cachedData = cachedData as? CachedChannelData {
|
about = cachedData.about
|
||||||
about = cachedData.about
|
} else {
|
||||||
} else {
|
about = nil
|
||||||
about = nil
|
}
|
||||||
|
participants.append(GroupCallParticipantsContext.Participant(
|
||||||
|
peer: myPeer,
|
||||||
|
ssrc: nil,
|
||||||
|
jsonParams: nil,
|
||||||
|
joinTimestamp: strongSelf.temporaryJoinTimestamp,
|
||||||
|
raiseHandRating: nil,
|
||||||
|
hasRaiseHand: false,
|
||||||
|
activityTimestamp: strongSelf.temporaryActivityTimestamp,
|
||||||
|
activityRank: strongSelf.temporaryActivityRank,
|
||||||
|
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
|
||||||
|
volume: nil,
|
||||||
|
about: about
|
||||||
|
))
|
||||||
|
participants.sort()
|
||||||
}
|
}
|
||||||
participants.insert(GroupCallParticipantsContext.Participant(
|
|
||||||
peer: myPeer,
|
|
||||||
ssrc: nil,
|
|
||||||
jsonParams: nil,
|
|
||||||
joinTimestamp: strongSelf.temporaryJoinTimestamp,
|
|
||||||
raiseHandRating: nil,
|
|
||||||
hasRaiseHand: false,
|
|
||||||
activityTimestamp: nil,
|
|
||||||
activityRank: nil,
|
|
||||||
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
|
|
||||||
volume: nil,
|
|
||||||
about: about
|
|
||||||
), at: 0)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
for participant in participants {
|
||||||
for participant in participants {
|
members.participants.append(participant)
|
||||||
members.participants.append(participant)
|
|
||||||
|
if topParticipants.count < 3 {
|
||||||
if topParticipants.count < 3 {
|
topParticipants.append(participant)
|
||||||
topParticipants.append(participant)
|
}
|
||||||
|
|
||||||
|
if let index = updatedInvitedPeers.firstIndex(of: participant.peer.id) {
|
||||||
|
updatedInvitedPeers.remove(at: index)
|
||||||
|
didUpdateInvitedPeers = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let index = updatedInvitedPeers.firstIndex(of: participant.peer.id) {
|
members.totalCount = state.totalCount
|
||||||
updatedInvitedPeers.remove(at: index)
|
members.loadMoreToken = state.nextParticipantsFetchOffset
|
||||||
didUpdateInvitedPeers = true
|
|
||||||
|
strongSelf.membersValue = members
|
||||||
|
|
||||||
|
var stateValue = strongSelf.stateValue
|
||||||
|
stateValue.myPeerId = strongSelf.joinAsPeerId
|
||||||
|
stateValue.adminIds = state.adminIds
|
||||||
|
|
||||||
|
strongSelf.stateValue = stateValue
|
||||||
|
|
||||||
|
strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState(
|
||||||
|
participantCount: state.totalCount,
|
||||||
|
topParticipants: topParticipants,
|
||||||
|
activeSpeakers: activeSpeakers
|
||||||
|
)))
|
||||||
|
|
||||||
|
if didUpdateInvitedPeers {
|
||||||
|
strongSelf.invitedPeersValue = updatedInvitedPeers
|
||||||
}
|
}
|
||||||
}
|
}))
|
||||||
|
} else {
|
||||||
members.totalCount = state.totalCount
|
self.temporaryParticipantsContext = nil
|
||||||
members.loadMoreToken = state.nextParticipantsFetchOffset
|
self.participantsContextStateDisposable.set((myPeer
|
||||||
|
|> deliverOnMainQueue
|
||||||
strongSelf.membersValue = members
|
|> take(1)).start(next: { [weak self] myPeerAndCachedData in
|
||||||
|
guard let strongSelf = self else {
|
||||||
var stateValue = strongSelf.stateValue
|
return
|
||||||
stateValue.myPeerId = strongSelf.joinAsPeerId
|
}
|
||||||
stateValue.adminIds = state.adminIds
|
|
||||||
|
var topParticipants: [GroupCallParticipantsContext.Participant] = []
|
||||||
strongSelf.stateValue = stateValue
|
|
||||||
|
var members = PresentationGroupCallMembers(
|
||||||
strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState(
|
participants: [],
|
||||||
participantCount: state.totalCount,
|
speakingParticipants: [],
|
||||||
topParticipants: topParticipants,
|
totalCount: 0,
|
||||||
activeSpeakers: activeSpeakers
|
loadMoreToken: nil
|
||||||
)))
|
)
|
||||||
|
|
||||||
if didUpdateInvitedPeers {
|
var participants: [GroupCallParticipantsContext.Participant] = []
|
||||||
strongSelf.invitedPeersValue = updatedInvitedPeers
|
|
||||||
}
|
if !participants.contains(where: { $0.peer.id == myPeerId }) {
|
||||||
}))
|
if let (myPeer, cachedData) = myPeerAndCachedData {
|
||||||
|
let about: String?
|
||||||
|
if let cachedData = cachedData as? CachedUserData {
|
||||||
|
about = cachedData.about
|
||||||
|
} else if let cachedData = cachedData as? CachedUserData {
|
||||||
|
about = cachedData.about
|
||||||
|
} else {
|
||||||
|
about = nil
|
||||||
|
}
|
||||||
|
participants.append(GroupCallParticipantsContext.Participant(
|
||||||
|
peer: myPeer,
|
||||||
|
ssrc: nil,
|
||||||
|
jsonParams: nil,
|
||||||
|
joinTimestamp: strongSelf.temporaryJoinTimestamp,
|
||||||
|
raiseHandRating: nil,
|
||||||
|
hasRaiseHand: false,
|
||||||
|
activityTimestamp: strongSelf.temporaryActivityTimestamp,
|
||||||
|
activityRank: strongSelf.temporaryActivityRank,
|
||||||
|
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
|
||||||
|
volume: nil,
|
||||||
|
about: about
|
||||||
|
))
|
||||||
|
participants.sort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for participant in participants {
|
||||||
|
members.participants.append(participant)
|
||||||
|
|
||||||
|
if topParticipants.count < 3 {
|
||||||
|
topParticipants.append(participant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.membersValue = members
|
||||||
|
|
||||||
|
var stateValue = strongSelf.stateValue
|
||||||
|
stateValue.myPeerId = strongSelf.joinAsPeerId
|
||||||
|
|
||||||
|
strongSelf.stateValue = stateValue
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateSessionState(internalState: InternalState, audioSessionControl: ManagedAudioSessionControl?) {
|
private func updateSessionState(internalState: InternalState, audioSessionControl: ManagedAudioSessionControl?) {
|
||||||
@ -1006,6 +1078,35 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let peerAdminIds: Signal<[PeerId], NoError>
|
||||||
|
let peerId = strongSelf.peerId
|
||||||
|
if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||||
|
peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in
|
||||||
|
var result: [PeerId] = []
|
||||||
|
if let entry = transaction.retrieveItemCacheEntry(id: cachedChannelAdminRanksEntryId(peerId: peerId)) as? CachedChannelAdminRanks {
|
||||||
|
result = Array(entry.ranks.keys)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in
|
||||||
|
var result: [PeerId] = []
|
||||||
|
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
|
||||||
|
if let participants = cachedData.participants {
|
||||||
|
for participant in participants.participants {
|
||||||
|
if case .creator = participant {
|
||||||
|
result.append(participant.peerId)
|
||||||
|
} else if case .admin = participant {
|
||||||
|
result.append(participant.peerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
strongSelf.currentLocalSsrc = ssrc
|
strongSelf.currentLocalSsrc = ssrc
|
||||||
strongSelf.requestDisposable.set((joinGroupCall(
|
strongSelf.requestDisposable.set((joinGroupCall(
|
||||||
account: strongSelf.account,
|
account: strongSelf.account,
|
||||||
@ -1015,6 +1116,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
accessHash: callInfo.accessHash,
|
accessHash: callInfo.accessHash,
|
||||||
preferMuted: true,
|
preferMuted: true,
|
||||||
joinPayload: joinPayload,
|
joinPayload: joinPayload,
|
||||||
|
peerAdminIds: peerAdminIds,
|
||||||
inviteHash: strongSelf.invite
|
inviteHash: strongSelf.invite
|
||||||
)
|
)
|
||||||
|> deliverOnMainQueue).start(next: { joinCallResult in
|
|> deliverOnMainQueue).start(next: { joinCallResult in
|
||||||
@ -1276,8 +1378,18 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
var updatedInvitedPeers = strongSelf.invitedPeersValue
|
var updatedInvitedPeers = strongSelf.invitedPeersValue
|
||||||
var didUpdateInvitedPeers = false
|
var didUpdateInvitedPeers = false
|
||||||
|
|
||||||
var participants = state.participants
|
var participants = state.participants
|
||||||
|
|
||||||
|
if let (ignorePeerId, ignoreSsrc) = strongSelf.ignorePreviousJoinAsPeerId {
|
||||||
|
for i in 0 ..< participants.count {
|
||||||
|
if participants[i].peer.id == ignorePeerId && participants[i].ssrc == ignoreSsrc {
|
||||||
|
participants.remove(at: i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !participants.contains(where: { $0.peer.id == myPeerId }) {
|
if !participants.contains(where: { $0.peer.id == myPeerId }) {
|
||||||
if let (myPeer, cachedData) = myPeerAndCachedData {
|
if let (myPeer, cachedData) = myPeerAndCachedData {
|
||||||
let about: String?
|
let about: String?
|
||||||
@ -1288,6 +1400,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
} else {
|
} else {
|
||||||
about = nil
|
about = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
participants.append(GroupCallParticipantsContext.Participant(
|
participants.append(GroupCallParticipantsContext.Participant(
|
||||||
peer: myPeer,
|
peer: myPeer,
|
||||||
ssrc: nil,
|
ssrc: nil,
|
||||||
@ -1295,8 +1408,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
joinTimestamp: strongSelf.temporaryJoinTimestamp,
|
joinTimestamp: strongSelf.temporaryJoinTimestamp,
|
||||||
raiseHandRating: nil,
|
raiseHandRating: nil,
|
||||||
hasRaiseHand: false,
|
hasRaiseHand: false,
|
||||||
activityTimestamp: nil,
|
activityTimestamp: strongSelf.temporaryActivityTimestamp,
|
||||||
activityRank: nil,
|
activityRank: strongSelf.temporaryActivityRank,
|
||||||
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
|
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
|
||||||
volume: nil,
|
volume: nil,
|
||||||
about: about
|
about: about
|
||||||
@ -1613,10 +1726,20 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let previousPeerId = strongSelf.joinAsPeerId
|
let previousPeerId = strongSelf.joinAsPeerId
|
||||||
|
if let localSsrc = strongSelf.currentLocalSsrc {
|
||||||
|
strongSelf.ignorePreviousJoinAsPeerId = (previousPeerId, localSsrc)
|
||||||
|
}
|
||||||
strongSelf.joinAsPeerId = peerId
|
strongSelf.joinAsPeerId = peerId
|
||||||
|
|
||||||
if let participantsContext = strongSelf.participantsContext, let immediateState = participantsContext.immediateState {
|
if let participantsContext = strongSelf.participantsContext, let immediateState = participantsContext.immediateState {
|
||||||
strongSelf.switchToTemporaryParticipantsContext(sourceContext: participantsContext, initialState: immediateState, oldMyPeerId: previousPeerId)
|
for participant in immediateState.participants {
|
||||||
|
if participant.peer.id == previousPeerId {
|
||||||
|
strongSelf.temporaryJoinTimestamp = participant.joinTimestamp
|
||||||
|
strongSelf.temporaryActivityTimestamp = participant.activityTimestamp
|
||||||
|
strongSelf.temporaryActivityRank = participant.activityRank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strongSelf.switchToTemporaryParticipantsContext(sourceContext: participantsContext, oldMyPeerId: previousPeerId)
|
||||||
} else {
|
} else {
|
||||||
strongSelf.stateValue.myPeerId = peerId
|
strongSelf.stateValue.myPeerId = peerId
|
||||||
}
|
}
|
||||||
|
|||||||
@ -276,7 +276,7 @@ private final class MainVideoContainerNode: ASDisplayNode {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.currentPeer = peer
|
self.currentPeer = peer
|
||||||
if let (peerId, source) = peer {
|
if let (_, source) = peer {
|
||||||
self.call.makeIncomingVideoView(source: source, completion: { [weak self] videoView in
|
self.call.makeIncomingVideoView(source: source, completion: { [weak self] videoView in
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
guard let strongSelf = self, let videoView = videoView else {
|
guard let strongSelf = self, let videoView = videoView else {
|
||||||
@ -704,6 +704,9 @@ public final class VoiceChatController: ViewController {
|
|||||||
|
|
||||||
private var requestedVideoSources = Set<UInt32>()
|
private var requestedVideoSources = Set<UInt32>()
|
||||||
private var videoNodes: [(PeerId, UInt32, GroupVideoNode)] = []
|
private var videoNodes: [(PeerId, UInt32, GroupVideoNode)] = []
|
||||||
|
|
||||||
|
private let displayAsPeersPromise = Promise<[FoundPeer]>([])
|
||||||
|
private let inviteLinksPromise = Promise<GroupCallInviteLinks?>(nil)
|
||||||
|
|
||||||
private var currentDominantSpeakerWithVideo: (PeerId, UInt32)?
|
private var currentDominantSpeakerWithVideo: (PeerId, UInt32)?
|
||||||
|
|
||||||
@ -729,9 +732,9 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor
|
self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor
|
||||||
self.backgroundNode.clipsToBounds = false
|
self.backgroundNode.clipsToBounds = false
|
||||||
|
|
||||||
if false {
|
/*if false {
|
||||||
self.mainVideoContainer = MainVideoContainerNode(context: call.accountContext, call: call)
|
self.mainVideoContainer = MainVideoContainerNode(context: call.accountContext, call: call)
|
||||||
}
|
}*/
|
||||||
|
|
||||||
self.listNode = ListView()
|
self.listNode = ListView()
|
||||||
self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3)
|
self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3)
|
||||||
@ -816,17 +819,15 @@ public final class VoiceChatController: ViewController {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
let displayAsPeersPromise = Promise<[FoundPeer]>([])
|
self.displayAsPeersPromise.set(displayAsPeers)
|
||||||
displayAsPeersPromise.set(displayAsPeers)
|
|
||||||
|
self.inviteLinksPromise.set(.single(nil)
|
||||||
let inviteLinksPromise = Promise<GroupCallInviteLinks?>(nil)
|
|
||||||
inviteLinksPromise.set(.single(nil)
|
|
||||||
|> then(call.inviteLinks))
|
|> then(call.inviteLinks))
|
||||||
|
|
||||||
self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in
|
self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in
|
||||||
let _ = self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
|
let _ = self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
|
||||||
}, openPeer: { [weak self] peerId in
|
}, openPeer: { [weak self] peerId in
|
||||||
if let strongSelf = self, let navigationController = strongSelf.controller?.parentNavigationController {
|
if let strongSelf = self {
|
||||||
/*let context = strongSelf.context
|
/*let context = strongSelf.context
|
||||||
strongSelf.controller?.dismiss(completion: {
|
strongSelf.controller?.dismiss(completion: {
|
||||||
Queue.mainQueue().justDispatch {
|
Queue.mainQueue().justDispatch {
|
||||||
@ -864,7 +865,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
return transaction.getPeer(groupPeerId)
|
return transaction.getPeer(groupPeerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = combineLatest(queue: Queue.mainQueue(), groupPeer, inviteLinksPromise.get() |> take(1)).start(next: { groupPeer, inviteLinks in
|
let _ = combineLatest(queue: Queue.mainQueue(), groupPeer, strongSelf.inviteLinksPromise.get() |> take(1)).start(next: { groupPeer, inviteLinks in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1368,7 +1369,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.call.state,
|
self.call.state,
|
||||||
self.call.members,
|
self.call.members,
|
||||||
invitedPeers,
|
invitedPeers,
|
||||||
displayAsPeersPromise.get()
|
self.displayAsPeersPromise.get()
|
||||||
)
|
)
|
||||||
|> mapToThrottled { values in
|
|> mapToThrottled { values in
|
||||||
return .single(values)
|
return .single(values)
|
||||||
@ -1514,272 +1515,9 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.audioOutputNode.addTarget(self, action: #selector(self.audioOutputPressed), forControlEvents: .touchUpInside)
|
self.audioOutputNode.addTarget(self, action: #selector(self.audioOutputPressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
self.cameraButtonNode.addTarget(self, action: #selector(self.cameraPressed), forControlEvents: .touchUpInside)
|
self.cameraButtonNode.addTarget(self, action: #selector(self.cameraPressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
|
||||||
self.optionsButton.contextAction = { [weak self] sourceNode, gesture in
|
self.optionsButton.contextAction = { [weak self] sourceNode, gesture in
|
||||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
self?.openContextMenu(sourceNode: sourceNode, gesture: gesture)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let canManageCall = !strongSelf.optionsButtonIsAvatar
|
|
||||||
|
|
||||||
let myPeerId = strongSelf.callState?.myPeerId
|
|
||||||
let darkTheme = strongSelf.darkTheme
|
|
||||||
|
|
||||||
var mainItemsImpl: (() -> Signal<[ContextMenuItem], NoError>)?
|
|
||||||
|
|
||||||
let displayAsItems: () -> Signal<[ContextMenuItem], NoError> = {
|
|
||||||
return displayAsPeersPromise.get()
|
|
||||||
|> take(1)
|
|
||||||
|> map { peers -> [ContextMenuItem] in
|
|
||||||
var items: [ContextMenuItem] = []
|
|
||||||
items.append(.custom(VoiceChatInfoContextItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAsInfo, icon: { theme in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Accounts"), color: theme.actionSheet.primaryTextColor)
|
|
||||||
}), true))
|
|
||||||
|
|
||||||
for peer in peers {
|
|
||||||
var subtitle: String?
|
|
||||||
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
|
|
||||||
subtitle = strongSelf.presentationData.strings.VoiceChat_PersonalAccount
|
|
||||||
} else if let subscribers = peer.subscribers {
|
|
||||||
subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers)
|
|
||||||
}
|
|
||||||
|
|
||||||
let isSelected = peer.peer.id == myPeerId
|
|
||||||
let extendedAvatarSize = CGSize(width: 35.0, height: 35.0)
|
|
||||||
let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)
|
|
||||||
|> map { image -> UIImage? in
|
|
||||||
if isSelected, let image = image {
|
|
||||||
return generateImage(extendedAvatarSize, rotatedContext: { size, context in
|
|
||||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
|
||||||
context.clear(bounds)
|
|
||||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
||||||
context.scaleBy(x: 1.0, y: -1.0)
|
|
||||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
||||||
context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height))
|
|
||||||
|
|
||||||
let lineWidth = 1.0 + UIScreenPixel
|
|
||||||
context.setLineWidth(lineWidth)
|
|
||||||
context.setStrokeColor(darkTheme.actionSheet.controlAccentColor.cgColor)
|
|
||||||
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { _, f in
|
|
||||||
f(.default)
|
|
||||||
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if peer.peer.id != myPeerId {
|
|
||||||
strongSelf.call.reconnect(as: peer.peer.id)
|
|
||||||
|
|
||||||
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer.peer, text: strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
|
|
||||||
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
|
|
||||||
items.append(.separator)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if canManageCall {
|
|
||||||
items.append(.separator)
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
|
||||||
}, action: { (c, _) in
|
|
||||||
if let mainItems = mainItemsImpl {
|
|
||||||
c.setItems(mainItems())
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let permissionItems: () -> Signal<[ContextMenuItem], NoError> = {
|
|
||||||
var items: [ContextMenuItem] = []
|
|
||||||
if let callState = strongSelf.callState, callState.canManageCall, let defaultParticipantMuteState = callState.defaultParticipantMuteState {
|
|
||||||
let isMuted = defaultParticipantMuteState == .muted
|
|
||||||
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in
|
|
||||||
if isMuted {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor)
|
|
||||||
}
|
|
||||||
}, action: { _, f in
|
|
||||||
f(.dismissWithoutContent)
|
|
||||||
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: false)
|
|
||||||
})))
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_SpeakPermissionAdmin, icon: { theme in
|
|
||||||
if !isMuted {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor)
|
|
||||||
}
|
|
||||||
}, action: { _, f in
|
|
||||||
f(.dismissWithoutContent)
|
|
||||||
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: true)
|
|
||||||
})))
|
|
||||||
items.append(.separator)
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
|
||||||
}, action: { (c, _) in
|
|
||||||
if let mainItems = mainItemsImpl {
|
|
||||||
c.setItems(mainItems())
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
return .single(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
mainItemsImpl = {
|
|
||||||
return combineLatest(displayAsPeersPromise.get(), context.account.postbox.loadedPeerWithId(call.peerId), inviteLinksPromise.get())
|
|
||||||
|> take(1)
|
|
||||||
|> map { peers, chatPeer, inviteLinks -> [ContextMenuItem] in
|
|
||||||
let presentationData = strongSelf.presentationData
|
|
||||||
var items: [ContextMenuItem] = []
|
|
||||||
|
|
||||||
if peers.count > 1 {
|
|
||||||
for peer in peers {
|
|
||||||
if peer.peer.id == myPeerId {
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, _ in
|
|
||||||
c.setItems(displayAsItems())
|
|
||||||
})))
|
|
||||||
items.append(.separator)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditTitle, icon: { theme -> UIImage? in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor)
|
|
||||||
}, action: { [weak self] _, f in
|
|
||||||
f(.default)
|
|
||||||
|
|
||||||
let controller = voiceChatTitleEditController(sharedContext: context.sharedContext, account: context.account, forceTheme: self?.darkTheme, title: presentationData.strings.VoiceChat_EditTitleTitle, text: presentationData.strings.VoiceChat_EditTitleText, placeholder: presentationData.strings.VoiceChat_Title, value: self?.callState?.title, apply: { [weak self] title in
|
|
||||||
if let strongSelf = self, let title = title {
|
|
||||||
strongSelf.call.updateTitle(title)
|
|
||||||
|
|
||||||
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
self?.controller?.present(controller, in: .window(.root))
|
|
||||||
})))
|
|
||||||
|
|
||||||
var hasPermissions = true
|
|
||||||
if let chatPeer = chatPeer as? TelegramChannel {
|
|
||||||
if case .broadcast = chatPeer.info {
|
|
||||||
hasPermissions = false
|
|
||||||
} else if chatPeer.flags.contains(.isGigagroup) {
|
|
||||||
hasPermissions = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hasPermissions {
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor)
|
|
||||||
}, action: { c, _ in
|
|
||||||
c.setItems(permissionItems())
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
if let inviteLinks = inviteLinks {
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
|
|
||||||
}, action: { [weak self] _, f in
|
|
||||||
f(.default)
|
|
||||||
|
|
||||||
self?.presentShare(inviteLinks)
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp {
|
|
||||||
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { _, f in
|
|
||||||
f(.dismissWithoutContent)
|
|
||||||
|
|
||||||
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StopRecordingStop, action: { [weak self] in
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.call.setShouldBeRecording(false, title: nil)
|
|
||||||
|
|
||||||
strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: strongSelf.presentationData.strings.VoiceChat_RecordingSaved), action: { _ in return false })
|
|
||||||
}
|
|
||||||
})])
|
|
||||||
self?.controller?.present(alertController, in: .window(.root))
|
|
||||||
}), false))
|
|
||||||
} else {
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in
|
|
||||||
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
|
|
||||||
}, action: { _, f in
|
|
||||||
f(.dismissWithoutContent)
|
|
||||||
|
|
||||||
|
|
||||||
let controller = voiceChatTitleEditController(sharedContext: context.sharedContext, account: context.account, forceTheme: self?.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, apply: { [weak self] title in
|
|
||||||
if let strongSelf = self, let title = title {
|
|
||||||
strongSelf.call.setShouldBeRecording(true, title: title)
|
|
||||||
|
|
||||||
strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
self?.controller?.present(controller, in: .window(.root))
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
if let callState = strongSelf.callState, callState.canManageCall {
|
|
||||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
|
|
||||||
}, action: { _, f in
|
|
||||||
f(.dismissWithoutContent)
|
|
||||||
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let action: () -> Void = {
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = (strongSelf.call.leave(terminateIfPossible: true)
|
|
||||||
|> filter { $0 }
|
|
||||||
|> take(1)
|
|
||||||
|> deliverOnMainQueue).start(completed: {
|
|
||||||
self?.controller?.dismiss()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle, text: strongSelf.presentationData.strings.VoiceChat_EndConfirmationText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.VoiceChat_EndConfirmationEnd, action: {
|
|
||||||
action()
|
|
||||||
})])
|
|
||||||
strongSelf.controller?.present(alertController, in: .window(.root))
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let items: Signal<[ContextMenuItem], NoError>
|
|
||||||
if canManageCall {
|
|
||||||
items = mainItemsImpl?() ?? .single([])
|
|
||||||
} else {
|
|
||||||
items = displayAsItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: strongSelf.optionsButton.referenceNode)), items: items, reactionItems: [], gesture: gesture)
|
|
||||||
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.optionsButton.addTarget(self, action: #selector(self.optionsPressed), forControlEvents: .touchUpInside)
|
self.optionsButton.addTarget(self, action: #selector(self.optionsPressed), forControlEvents: .touchUpInside)
|
||||||
@ -1933,6 +1671,307 @@ public final class VoiceChatController: ViewController {
|
|||||||
self.memberEventsDisposable.dispose()
|
self.memberEventsDisposable.dispose()
|
||||||
self.voiceSourcesDisposable.dispose()
|
self.voiceSourcesDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openContextMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
|
||||||
|
let canManageCall = !self.optionsButtonIsAvatar
|
||||||
|
|
||||||
|
let items: Signal<[ContextMenuItem], NoError>
|
||||||
|
if canManageCall {
|
||||||
|
items = self.contextMenuMainItems()
|
||||||
|
} else {
|
||||||
|
items = self.contextMenuDisplayAsItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let controller = self.controller {
|
||||||
|
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: self.optionsButton.referenceNode)), items: items, reactionItems: [], gesture: gesture)
|
||||||
|
controller.presentInGlobalOverlay(contextController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> {
|
||||||
|
guard let myPeerId = self.callState?.myPeerId else {
|
||||||
|
return .single([])
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
||||||
|
|
||||||
|
return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(call.peerId), self.inviteLinksPromise.get())
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue
|
||||||
|
|> map { [weak self] peers, chatPeer, inviteLinks -> [ContextMenuItem] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = strongSelf.presentationData
|
||||||
|
var items: [ContextMenuItem] = []
|
||||||
|
|
||||||
|
if peers.count > 1 {
|
||||||
|
for peer in peers {
|
||||||
|
if peer.peer.id == myPeerId {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, _ in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.setItems(strongSelf.contextMenuDisplayAsItems())
|
||||||
|
})))
|
||||||
|
items.append(.separator)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditTitle, icon: { theme -> UIImage? in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { _, f in
|
||||||
|
f(.default)
|
||||||
|
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_EditTitleTitle, text: presentationData.strings.VoiceChat_EditTitleText, placeholder: presentationData.strings.VoiceChat_Title, value: strongSelf.callState?.title, apply: { title in
|
||||||
|
if let strongSelf = self, let title = title {
|
||||||
|
strongSelf.call.updateTitle(title)
|
||||||
|
|
||||||
|
strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: title.isEmpty ? strongSelf.presentationData.strings.VoiceChat_EditTitleRemoveSuccess : strongSelf.presentationData.strings.VoiceChat_EditTitleSuccess(title).0), action: { _ in return false })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self?.controller?.present(controller, in: .window(.root))
|
||||||
|
})))
|
||||||
|
|
||||||
|
var hasPermissions = true
|
||||||
|
if let chatPeer = chatPeer as? TelegramChannel {
|
||||||
|
if case .broadcast = chatPeer.info {
|
||||||
|
hasPermissions = false
|
||||||
|
} else if chatPeer.flags.contains(.isGigagroup) {
|
||||||
|
hasPermissions = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasPermissions {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { c, _ in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.setItems(strongSelf.contextMenuPermissionItems())
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let inviteLinks = inviteLinks {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_Share, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { _, f in
|
||||||
|
f(.default)
|
||||||
|
|
||||||
|
self?.presentShare(inviteLinks)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp {
|
||||||
|
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { _, f in
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StopRecordingStop, action: {
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.call.setShouldBeRecording(false, title: nil)
|
||||||
|
|
||||||
|
strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: strongSelf.presentationData.strings.VoiceChat_RecordingSaved), action: { _ in return false })
|
||||||
|
}
|
||||||
|
})])
|
||||||
|
self?.controller?.present(alertController, in: .window(.root))
|
||||||
|
}), false))
|
||||||
|
} else {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in
|
||||||
|
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { _, f in
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, apply: { title in
|
||||||
|
if let strongSelf = self, let title = title {
|
||||||
|
strongSelf.call.setShouldBeRecording(true, title: title)
|
||||||
|
|
||||||
|
strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self?.controller?.present(controller, in: .window(.root))
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let callState = strongSelf.callState, callState.canManageCall {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
|
||||||
|
}, action: { _, f in
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let action: () -> Void = {
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (strongSelf.call.leave(terminateIfPossible: true)
|
||||||
|
|> filter { $0 }
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue).start(completed: {
|
||||||
|
self?.controller?.dismiss()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: strongSelf.presentationData.strings.VoiceChat_EndConfirmationTitle, text: strongSelf.presentationData.strings.VoiceChat_EndConfirmationText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.VoiceChat_EndConfirmationEnd, action: {
|
||||||
|
action()
|
||||||
|
})])
|
||||||
|
strongSelf.controller?.present(alertController, in: .window(.root))
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func contextMenuDisplayAsItems() -> Signal<[ContextMenuItem], NoError> {
|
||||||
|
guard let myPeerId = self.callState?.myPeerId else {
|
||||||
|
return .single([])
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarSize = CGSize(width: 28.0, height: 28.0)
|
||||||
|
let canManageCall = !self.optionsButtonIsAvatar
|
||||||
|
let darkTheme = self.darkTheme
|
||||||
|
|
||||||
|
return self.displayAsPeersPromise.get()
|
||||||
|
|> take(1)
|
||||||
|
|> map { [weak self] peers -> [ContextMenuItem] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
var items: [ContextMenuItem] = []
|
||||||
|
items.append(.custom(VoiceChatInfoContextItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAsInfo, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Accounts"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}), true))
|
||||||
|
|
||||||
|
for peer in peers {
|
||||||
|
var subtitle: String?
|
||||||
|
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
|
||||||
|
subtitle = strongSelf.presentationData.strings.VoiceChat_PersonalAccount
|
||||||
|
} else if let subscribers = peer.subscribers {
|
||||||
|
subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers)
|
||||||
|
}
|
||||||
|
|
||||||
|
let isSelected = peer.peer.id == myPeerId
|
||||||
|
let extendedAvatarSize = CGSize(width: 35.0, height: 35.0)
|
||||||
|
let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)
|
||||||
|
|> map { image -> UIImage? in
|
||||||
|
if isSelected, let image = image {
|
||||||
|
return generateImage(extendedAvatarSize, rotatedContext: { size, context in
|
||||||
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||||
|
context.clear(bounds)
|
||||||
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||||
|
context.scaleBy(x: 1.0, y: -1.0)
|
||||||
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||||
|
context.draw(image.cgImage!, in: CGRect(x: (extendedAvatarSize.width - avatarSize.width) / 2.0, y: (extendedAvatarSize.height - avatarSize.height) / 2.0, width: avatarSize.width, height: avatarSize.height))
|
||||||
|
|
||||||
|
let lineWidth = 1.0 + UIScreenPixel
|
||||||
|
context.setLineWidth(lineWidth)
|
||||||
|
context.setStrokeColor(darkTheme.actionSheet.controlAccentColor.cgColor)
|
||||||
|
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { _, f in
|
||||||
|
f(.default)
|
||||||
|
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer.peer.id != myPeerId {
|
||||||
|
strongSelf.call.reconnect(as: peer.peer.id)
|
||||||
|
|
||||||
|
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer.peer, text: strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
if peer.peer.id.namespace == Namespaces.Peer.CloudUser {
|
||||||
|
items.append(.separator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if canManageCall {
|
||||||
|
items.append(.separator)
|
||||||
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { (c, _) in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.setItems(strongSelf.contextMenuMainItems())
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func contextMenuPermissionItems() -> Signal<[ContextMenuItem], NoError> {
|
||||||
|
var items: [ContextMenuItem] = []
|
||||||
|
if let callState = self.callState, callState.canManageCall, let defaultParticipantMuteState = callState.defaultParticipantMuteState {
|
||||||
|
let isMuted = defaultParticipantMuteState == .muted
|
||||||
|
|
||||||
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.VoiceChat_SpeakPermissionEveryone, icon: { theme in
|
||||||
|
if isMuted {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: false)
|
||||||
|
})))
|
||||||
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.VoiceChat_SpeakPermissionAdmin, icon: { theme in
|
||||||
|
if !isMuted {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: true)
|
||||||
|
})))
|
||||||
|
items.append(.separator)
|
||||||
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { [weak self] (c, _) in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.setItems(strongSelf.contextMenuMainItems())
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
return .single(items)
|
||||||
|
}
|
||||||
|
|
||||||
override func didLoad() {
|
override func didLoad() {
|
||||||
super.didLoad()
|
super.didLoad()
|
||||||
@ -3322,7 +3361,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
|
|
||||||
self.reclaimActionButton = { [weak self, weak overlayController] in
|
self.reclaimActionButton = { [weak self, weak overlayController] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
overlayController?.animateOut(reclaim: true, completion: { [weak self] immediate in
|
overlayController?.animateOut(reclaim: true, completion: { immediate in
|
||||||
if let strongSelf = self, immediate {
|
if let strongSelf = self, immediate {
|
||||||
strongSelf.controllerNode.actionButton.ignoreHierarchyChanges = true
|
strongSelf.controllerNode.actionButton.ignoreHierarchyChanges = true
|
||||||
strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.actionButton)
|
strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.actionButton)
|
||||||
|
|||||||
@ -356,7 +356,7 @@ public struct JoinGroupCallResult {
|
|||||||
public var connectionMode: ConnectionMode
|
public var connectionMode: ConnectionMode
|
||||||
}
|
}
|
||||||
|
|
||||||
public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, callId: Int64, accessHash: Int64, preferMuted: Bool, joinPayload: String, inviteHash: String? = nil) -> Signal<JoinGroupCallResult, JoinGroupCallError> {
|
public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, callId: Int64, accessHash: Int64, preferMuted: Bool, joinPayload: String, peerAdminIds: Signal<[PeerId], NoError>, inviteHash: String? = nil) -> Signal<JoinGroupCallResult, JoinGroupCallError> {
|
||||||
return account.postbox.transaction { transaction -> Api.InputPeer? in
|
return account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||||
if let joinAs = joinAs {
|
if let joinAs = joinAs {
|
||||||
return transaction.getPeer(joinAs).flatMap(apiInputPeer)
|
return transaction.getPeer(joinAs).flatMap(apiInputPeer)
|
||||||
@ -377,8 +377,8 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal
|
|||||||
if let _ = inviteHash {
|
if let _ = inviteHash {
|
||||||
flags |= (1 << 1)
|
flags |= (1 << 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return account.network.request(Api.functions.phone.joinGroupCall(flags: flags, call: .inputGroupCall(id: callId, accessHash: accessHash), joinAs: inputJoinAs, inviteHash: inviteHash, params: .dataJSON(data: joinPayload)))
|
let joinRequest = account.network.request(Api.functions.phone.joinGroupCall(flags: flags, call: .inputGroupCall(id: callId, accessHash: accessHash), joinAs: inputJoinAs, inviteHash: inviteHash, params: .dataJSON(data: joinPayload)))
|
||||||
|> mapError { error -> JoinGroupCallError in
|
|> mapError { error -> JoinGroupCallError in
|
||||||
if error.errorDescription == "GROUPCALL_ANONYMOUS_FORBIDDEN" {
|
if error.errorDescription == "GROUPCALL_ANONYMOUS_FORBIDDEN" {
|
||||||
return .anonymousNotAllowed
|
return .anonymousNotAllowed
|
||||||
@ -387,92 +387,32 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal
|
|||||||
}
|
}
|
||||||
return .generic
|
return .generic
|
||||||
}
|
}
|
||||||
|> mapToSignal { updates -> Signal<JoinGroupCallResult, JoinGroupCallError> in
|
|
||||||
let admins: Signal<(Set<PeerId>, [Api.User]), JoinGroupCallError>
|
let getParticipantsRequest = getGroupCallParticipants(account: account, callId: callId, accessHash: accessHash, offset: "", ssrcs: [], limit: 100)
|
||||||
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
|> mapError { _ -> JoinGroupCallError in
|
||||||
admins = account.postbox.transaction { transaction -> Api.InputChannel? in
|
return .generic
|
||||||
return transaction.getPeer(peerId).flatMap(apiInputChannel)
|
}
|
||||||
}
|
|
||||||
|> castError(JoinGroupCallError.self)
|
return combineLatest(
|
||||||
|> mapToSignal { inputChannel -> Signal<Api.channels.ChannelParticipants, JoinGroupCallError> in
|
joinRequest,
|
||||||
guard let inputChannel = inputChannel else {
|
getParticipantsRequest
|
||||||
return .fail(.generic)
|
)
|
||||||
}
|
|> mapToSignal { updates, participantsState -> Signal<JoinGroupCallResult, JoinGroupCallError> in
|
||||||
|
|
||||||
return account.network.request(Api.functions.channels.getParticipants(channel: inputChannel, filter: .channelParticipantsAdmins, offset: 0, limit: 100, hash: 0))
|
|
||||||
|> `catch` { _ in
|
|
||||||
return .single(.channelParticipantsNotModified)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|> map { admins -> (Set<PeerId>, [Api.User]) in
|
|
||||||
var adminIds = Set<PeerId>()
|
|
||||||
var apiUsers: [Api.User] = []
|
|
||||||
|
|
||||||
switch admins {
|
|
||||||
case let .channelParticipants(_, participants, users):
|
|
||||||
apiUsers.append(contentsOf: users)
|
|
||||||
|
|
||||||
for participant in participants {
|
|
||||||
let parsedParticipant = ChannelParticipant(apiParticipant: participant)
|
|
||||||
switch parsedParticipant {
|
|
||||||
case .creator:
|
|
||||||
adminIds.insert(parsedParticipant.peerId)
|
|
||||||
case let .member(_, _, adminInfo, _, _):
|
|
||||||
if let adminInfo = adminInfo, adminInfo.rights.rights.contains(.canManageCalls) {
|
|
||||||
adminIds.insert(parsedParticipant.peerId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return (adminIds, apiUsers)
|
|
||||||
}
|
|
||||||
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
|
|
||||||
admins = account.postbox.transaction { transaction -> (Set<PeerId>, [Api.User]) in
|
|
||||||
var result = Set<PeerId>()
|
|
||||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
|
|
||||||
if let participants = cachedData.participants {
|
|
||||||
for participant in participants.participants {
|
|
||||||
if case .creator = participant {
|
|
||||||
result.insert(participant.peerId)
|
|
||||||
} else if case .admin = participant {
|
|
||||||
result.insert(participant.peerId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (result, [])
|
|
||||||
}
|
|
||||||
|> castError(JoinGroupCallError.self)
|
|
||||||
} else {
|
|
||||||
admins = .fail(.generic)
|
|
||||||
}
|
|
||||||
|
|
||||||
let peer = account.postbox.transaction { transaction -> Peer? in
|
let peer = account.postbox.transaction { transaction -> Peer? in
|
||||||
return transaction.getPeer(peerId)
|
return transaction.getPeer(peerId)
|
||||||
}
|
}
|
||||||
|> castError(JoinGroupCallError.self)
|
|> castError(JoinGroupCallError.self)
|
||||||
|
|
||||||
return combineLatest(
|
return combineLatest(
|
||||||
account.network.request(Api.functions.phone.getGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash)))
|
peerAdminIds |> castError(JoinGroupCallError.self) |> take(1),
|
||||||
|> mapError { _ -> JoinGroupCallError in
|
|
||||||
return .generic
|
|
||||||
},
|
|
||||||
getGroupCallParticipants(account: account, callId: callId, accessHash: accessHash, offset: "", ssrcs: [], limit: 100)
|
|
||||||
|> mapError { _ -> JoinGroupCallError in
|
|
||||||
return .generic
|
|
||||||
},
|
|
||||||
admins,
|
|
||||||
peer
|
peer
|
||||||
)
|
)
|
||||||
|> mapToSignal { result, state, admins, peer -> Signal<JoinGroupCallResult, JoinGroupCallError> in
|
|> mapToSignal { peerAdminIds, peer -> Signal<JoinGroupCallResult, JoinGroupCallError> in
|
||||||
guard let peer = peer else {
|
guard let peer = peer else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = state
|
var state = participantsState
|
||||||
if let channel = peer as? TelegramChannel {
|
if let channel = peer as? TelegramChannel {
|
||||||
state.isCreator = channel.flags.contains(.isCreator)
|
state.isCreator = channel.flags.contains(.isCreator)
|
||||||
} else if let group = peer as? TelegramGroup {
|
} else if let group = peer as? TelegramGroup {
|
||||||
@ -514,71 +454,53 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal
|
|||||||
|
|
||||||
var apiUsers: [Api.User] = []
|
var apiUsers: [Api.User] = []
|
||||||
|
|
||||||
let (adminIds, adminUsers) = admins
|
state.adminIds = Set(peerAdminIds)
|
||||||
apiUsers.append(contentsOf: adminUsers)
|
|
||||||
|
var peers: [Peer] = []
|
||||||
state.adminIds = adminIds
|
var peerPresences: [PeerId: PeerPresence] = [:]
|
||||||
|
|
||||||
switch result {
|
for user in apiUsers {
|
||||||
case let .groupCall(call, _, _, chats, users):
|
let telegramUser = TelegramUser(user: user)
|
||||||
guard let _ = GroupCallInfo(call) else {
|
peers.append(telegramUser)
|
||||||
return .fail(.generic)
|
if let presence = TelegramUserPresence(apiUser: user) {
|
||||||
|
peerPresences[telegramUser.id] = presence
|
||||||
}
|
}
|
||||||
|
|
||||||
apiUsers.append(contentsOf: users)
|
|
||||||
|
|
||||||
var peers: [Peer] = []
|
|
||||||
var peerPresences: [PeerId: PeerPresence] = [:]
|
|
||||||
|
|
||||||
for user in apiUsers {
|
|
||||||
let telegramUser = TelegramUser(user: user)
|
|
||||||
peers.append(telegramUser)
|
|
||||||
if let presence = TelegramUserPresence(apiUser: user) {
|
|
||||||
peerPresences[telegramUser.id] = presence
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for chat in chats {
|
|
||||||
if let peer = parseTelegramGroupOrChannel(chat: chat) {
|
|
||||||
peers.append(peer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let connectionMode: JoinGroupCallResult.ConnectionMode
|
|
||||||
if let clientParams = parsedCall.clientParams, let clientParamsData = clientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: clientParamsData, options: [])) as? [String: Any] {
|
|
||||||
if let stream = dict["stream"] as? Bool, stream {
|
|
||||||
connectionMode = .broadcast
|
|
||||||
} else {
|
|
||||||
connectionMode = .rtc
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
connectionMode = .broadcast
|
|
||||||
}
|
|
||||||
|
|
||||||
return account.postbox.transaction { transaction -> JoinGroupCallResult in
|
|
||||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
|
||||||
if let cachedData = cachedData as? CachedChannelData {
|
|
||||||
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
|
||||||
} else if let cachedData = cachedData as? CachedGroupData {
|
|
||||||
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
|
||||||
} else {
|
|
||||||
return cachedData
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
|
||||||
return updated
|
|
||||||
})
|
|
||||||
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
|
|
||||||
|
|
||||||
return JoinGroupCallResult(
|
|
||||||
callInfo: parsedCall,
|
|
||||||
state: state,
|
|
||||||
connectionMode: connectionMode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|> castError(JoinGroupCallError.self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let connectionMode: JoinGroupCallResult.ConnectionMode
|
||||||
|
if let clientParams = parsedCall.clientParams, let clientParamsData = clientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: clientParamsData, options: [])) as? [String: Any] {
|
||||||
|
if let stream = dict["stream"] as? Bool, stream {
|
||||||
|
connectionMode = .broadcast
|
||||||
|
} else {
|
||||||
|
connectionMode = .rtc
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
connectionMode = .broadcast
|
||||||
|
}
|
||||||
|
|
||||||
|
return account.postbox.transaction { transaction -> JoinGroupCallResult in
|
||||||
|
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in
|
||||||
|
if let cachedData = cachedData as? CachedChannelData {
|
||||||
|
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
||||||
|
} else if let cachedData = cachedData as? CachedGroupData {
|
||||||
|
return cachedData.withUpdatedCallJoinPeerId(joinAs)
|
||||||
|
} else {
|
||||||
|
return cachedData
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
|
||||||
|
|
||||||
|
return JoinGroupCallResult(
|
||||||
|
callInfo: parsedCall,
|
||||||
|
state: state,
|
||||||
|
connectionMode: connectionMode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|> castError(JoinGroupCallError.self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,7 +87,15 @@ extension TelegramUser {
|
|||||||
if !isMin {
|
if !isMin {
|
||||||
return TelegramUser(user: rhs)
|
return TelegramUser(user: rhs)
|
||||||
} else {
|
} else {
|
||||||
let telegramPhoto = photo.flatMap(parsedTelegramProfilePhoto) ?? []
|
let telegramPhoto: [TelegramMediaImageRepresentation]
|
||||||
|
if let photo = photo {
|
||||||
|
telegramPhoto = parsedTelegramProfilePhoto(photo)
|
||||||
|
} else if let currentPhoto = lhs?.photo {
|
||||||
|
telegramPhoto = currentPhoto
|
||||||
|
} else {
|
||||||
|
telegramPhoto = []
|
||||||
|
}
|
||||||
|
|
||||||
if let lhs = lhs {
|
if let lhs = lhs {
|
||||||
var userFlags: UserInfoFlags = []
|
var userFlags: UserInfoFlags = []
|
||||||
if (flags & (1 << 17)) != 0 {
|
if (flags & (1 << 17)) != 0 {
|
||||||
|
|||||||
@ -465,9 +465,15 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
|||||||
}
|
}
|
||||||
case let .joinVoiceChat(peerId, invite):
|
case let .joinVoiceChat(peerId, invite):
|
||||||
dismissInput()
|
dismissInput()
|
||||||
openPeer(peerId, defaultNavigationForPeerId(peerId, navigation: .default))
|
if let navigationController = navigationController {
|
||||||
present(VoiceChatJoinScreen(context: context, peerId: peerId, invite: invite, join: { call in
|
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), completion: { chatController in
|
||||||
joinVoiceChat?(peerId, invite, call)
|
guard let chatController = chatController as? ChatControllerImpl else {
|
||||||
}), nil)
|
return
|
||||||
|
}
|
||||||
|
navigationController.currentWindow?.present(VoiceChatJoinScreen(context: context, peerId: peerId, invite: invite, join: { [weak chatController] call in
|
||||||
|
chatController?.joinGroupCall(peerId: peerId, invite: invite, activeCall: call)
|
||||||
|
}), on: .root, blockInteraction: false, completion: {})
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1039,9 +1039,9 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
|||||||
if channel.flags.contains(.hasVoiceChat) {
|
if channel.flags.contains(.hasVoiceChat) {
|
||||||
hasVoiceChat = true
|
hasVoiceChat = true
|
||||||
}
|
}
|
||||||
if !hasVoiceChat && (channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls)) {
|
}
|
||||||
canStartVoiceChat = true
|
if !hasVoiceChat && (channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls)) {
|
||||||
}
|
canStartVoiceChat = true
|
||||||
}
|
}
|
||||||
switch channel.participationStatus {
|
switch channel.participationStatus {
|
||||||
case .member:
|
case .member:
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit 4a953747375b8648f8b66e9572b59b10f7b769a1
|
Subproject commit 30070e3d277debf4a69e0df001faffe571465614
|
||||||
Loading…
x
Reference in New Issue
Block a user