Add voice chat invite links support

This commit is contained in:
Ilya Laktyushin 2021-03-10 13:41:32 +04:00
commit 259491941f
26 changed files with 3420 additions and 3175 deletions

View File

@ -5768,6 +5768,7 @@ Sorry for the inconvenience.";
"VoiceChat.EndConfirmationEnd" = "End";
"VoiceChat.InviteMemberToGroupFirstText" = "%1$@ isn't a member of \"%2$@\" yet. Add them to the group?";
"VoiceChat.InviteMemberToChannelFirstText" = "%1$@ isn't a member of \"%2$@\" yet. Add them to the channel?";
"VoiceChat.InviteMemberToGroupFirstAdd" = "Add";
"VoiceChat.CreateNewVoiceChatText" = "Voice chat ended. Start a new one?";

View File

@ -328,6 +328,8 @@ public protocol PresentationGroupCall: class {
func removedPeer(_ peerId: PeerId)
var invitedPeers: Signal<[PeerId], NoError> { get }
var inviteLinks: Signal<GroupCallInviteLinks?, NoError> { get }
var incomingVideoSources: Signal<[PeerId: UInt32], NoError> { get }
func makeIncomingVideoView(source: UInt32, completion: @escaping (PresentationCallVideoView?) -> Void)

View File

@ -12,6 +12,7 @@ import ItemListUI
import PresentationDataUtils
import AccountContext
import TelegramNotices
import ChatListSearchItemHeader
private struct CallListNodeListViewTransition {
let callListView: CallListNodeView
@ -567,6 +568,12 @@ final class CallListControllerNode: ASDisplayNode {
self.updateState {
return $0.withUpdatedPresentationData(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: presentationData.disableAnimations)
}
self.listNode.forEachItemHeaderNode({ itemHeaderNode in
if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode {
itemHeaderNode.updateTheme(theme: presentationData.theme)
}
})
}
}

View File

@ -61,8 +61,6 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
}
return self.delayedFrames.remove(at: minFrameIndex)
}
} else {
assert(true)
}
return nil

View File

@ -865,7 +865,7 @@ private final class ChatListViewSpaceState {
let loadedEntries = postbox.chatListTable.entries(groupId: .root, from: (allEntries[0].index.predecessor, true), to: (allEntries[allEntries.count - 1].index.successor, true), peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: 1000, predicate: nil).map(mapEntry)
assert(loadedEntries.map({ $0.index }) == allEntries.map({ $0.index }))
//assert(loadedEntries.map({ $0.index }) == allEntries.map({ $0.index }))
}
}
#endif

View File

@ -357,7 +357,14 @@ public final class ShareController: ViewController {
switch subject {
case let .url(text):
self.defaultAction = ShareControllerAction(title: forcedActionTitle ?? self.presentationData.strings.ShareMenu_CopyShareLink, action: { [weak self] in
UIPasteboard.general.string = text
if let strongSelf = self, let segmentedValues = segmentedValues {
let selectedValue = segmentedValues[strongSelf.controllerNode.selectedSegmentedIndex]
if case let .url(text) = selectedValue.subject {
UIPasteboard.general.string = text
}
} else {
UIPasteboard.general.string = text
}
self?.controllerNode.cancel?()
self?.actionCompleted?()
@ -499,7 +506,13 @@ public final class ShareController: ViewController {
}
var shareSignals: [Signal<[MessageId?], NoError>] = []
switch strongSelf.subject {
var subject = strongSelf.subject
if let segmentedValues = strongSelf.segmentedValues {
let selectedValue = segmentedValues[strongSelf.controllerNode.selectedSegmentedIndex]
subject = selectedValue.subject
}
switch subject {
case let .url(url):
for peerId in peerIds {
var messages: [EnqueueMessage] = []
@ -635,7 +648,12 @@ public final class ShareController: ViewController {
self.controllerNode.shareExternal = { [weak self] in
if let strongSelf = self {
var collectableItems: [CollectableExternalShareItem] = []
switch strongSelf.subject {
var subject = strongSelf.subject
if let segmentedValues = strongSelf.segmentedValues {
let selectedValue = segmentedValues[strongSelf.controllerNode.selectedSegmentedIndex]
subject = selectedValue.subject
}
switch subject {
case let .url(text):
collectableItems.append(CollectableExternalShareItem(url: explicitUrl(text), text: "", author: nil, timestamp: nil, mediaReference: nil))
case let .text(string):

View File

@ -31,7 +31,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
private var immediatePeerId: PeerId?
private let fromForeignApp: Bool
private let segmentedValues: [ShareControllerSegmentedValue]?
private var selectedSegmentedIndex: Int = 0
var selectedSegmentedIndex: Int = 0
private let defaultAction: ShareControllerAction?
private let requestLayout: (ContainedViewLayoutTransition) -> Void
@ -237,8 +237,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
self.contentContainerNode.addSubnode(self.actionSeparatorNode)
self.contentContainerNode.addSubnode(self.actionsBackgroundNode)
self.contentContainerNode.addSubnode(self.actionButtonNode)
self.contentContainerNode.addSubnode(self.inputFieldNode)
self.contentContainerNode.addSubnode(self.actionButtonNode)
self.inputFieldNode.updateHeight = { [weak self] in
if let strongSelf = self {
@ -314,7 +314,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
let previousAlpha = node.alpha
node.alpha = alpha
if animated {
node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: alpha.isZero ? 0.18 : 0.32)
node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: alpha.isZero ? 0.18 : 0.32, timingFunction: alpha.isZero ? CAMediaTimingFunctionName.easeOut.rawValue : CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
if let inputNode = node as? ShareInputFieldNode, alpha.isZero {
@ -460,13 +460,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
transition.updateFrame(node: self.actionsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: bottomGridInset)))
transition.updateFrame(node: self.actionsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: bottomGridInset)), beginWithCurrentState: true)
transition.updateFrame(node: self.actionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - actionButtonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight)))
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: inputHeight)))
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset), size: CGSize(width: contentContainerFrame.size.width, height: inputHeight)), beginWithCurrentState: true)
transition.updateFrame(node: self.actionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)))
transition.updateFrame(node: self.actionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - bottomGridInset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)), beginWithCurrentState: true)
let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight))

View File

@ -672,6 +672,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[286776671] = { return Api.GeoPoint.parse_geoPointEmpty($0) }
dict[-1297942941] = { return Api.GeoPoint.parse_geoPoint($0) }
dict[506920429] = { return Api.InputPhoneCall.parse_inputPhoneCall($0) }
dict[541839704] = { return Api.phone.ExportedGroupCallInvite.parse_exportedGroupCallInvite($0) }
dict[-1551583367] = { return Api.ReceivedNotifyMessage.parse_receivedNotifyMessage($0) }
dict[-57668565] = { return Api.ChatParticipants.parse_chatParticipantsForbidden($0) }
dict[1061556205] = { return Api.ChatParticipants.parse_chatParticipants($0) }
@ -1413,6 +1414,8 @@ public struct Api {
_1.serialize(buffer, boxed)
case let _1 as Api.InputPhoneCall:
_1.serialize(buffer, boxed)
case let _1 as Api.phone.ExportedGroupCallInvite:
_1.serialize(buffer, boxed)
case let _1 as Api.ReceivedNotifyMessage:
_1.serialize(buffer, boxed)
case let _1 as Api.ChatParticipants:

View File

@ -1811,6 +1811,40 @@ public struct phone {
}
}
}
public enum ExportedGroupCallInvite: TypeConstructorDescription {
case exportedGroupCallInvite(link: String)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .exportedGroupCallInvite(let link):
if boxed {
buffer.appendInt32(541839704)
}
serializeString(link, buffer: buffer, boxed: false)
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .exportedGroupCallInvite(let link):
return ("exportedGroupCallInvite", [("link", link)])
}
}
public static func parse_exportedGroupCallInvite(_ reader: BufferReader) -> ExportedGroupCallInvite? {
var _1: String?
_1 = parseString(reader)
let _c1 = _1 != nil
if _c1 {
return Api.phone.ExportedGroupCallInvite.exportedGroupCallInvite(link: _1!)
}
else {
return nil
}
}
}
public enum PhoneCall: TypeConstructorDescription {
case phoneCall(phoneCall: Api.PhoneCall, users: [Api.User])
@ -7703,17 +7737,16 @@ public extension Api {
})
}
public static func inviteToGroupCall(flags: Int32, call: Api.InputGroupCall, users: [Api.InputUser]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
public static func inviteToGroupCall(call: Api.InputGroupCall, users: [Api.InputUser]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(-919505530)
serializeInt32(flags, buffer: buffer, boxed: false)
buffer.appendInt32(2067345760)
call.serialize(buffer, true)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(users.count))
for item in users {
item.serialize(buffer, true)
}
return (FunctionDescription(name: "phone.inviteToGroupCall", parameters: [("flags", flags), ("call", call), ("users", users)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
return (FunctionDescription(name: "phone.inviteToGroupCall", parameters: [("call", call), ("users", users)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {
@ -7870,6 +7903,21 @@ public extension Api {
return result
})
}
public static func exportGroupCallInvite(flags: Int32, call: Api.InputGroupCall) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.phone.ExportedGroupCallInvite>) {
let buffer = Buffer()
buffer.appendInt32(-425040769)
serializeInt32(flags, buffer: buffer, boxed: false)
call.serialize(buffer, true)
return (FunctionDescription(name: "phone.exportGroupCallInvite", parameters: [("flags", flags), ("call", call)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.phone.ExportedGroupCallInvite? in
let reader = BufferReader(buffer)
var result: Api.phone.ExportedGroupCallInvite?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.phone.ExportedGroupCallInvite
}
return result
})
}
}
}
}

View File

@ -842,14 +842,7 @@ public final class ManagedAudioSession {
private func updateOutputMode(_ outputMode: AudioSessionOutputMode) {
if let (type, currentOutputMode) = self.currentTypeAndOutputMode, currentOutputMode != outputMode {
//self.currentTypeAndOutputMode = (type, outputMode)
do {
try self.setup(type: type, outputMode: outputMode, activateNow: true)
//try self.setupOutputMode(outputMode, type: type)
//try self.activate()
} catch let error {
print("ManagedAudioSession overrideOutputAudioPort error \(error)")
}
self.setup(type: type, outputMode: outputMode, activateNow: true)
}
}

View File

@ -711,7 +711,6 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
initialCall: initialCall,
internalId: internalId,
peerId: peerId,
peer: nil,
joinAsPeerId: joinAsPeerId
)
strongSelf.updateCurrentGroupCall(call)

View File

@ -195,7 +195,7 @@ public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCach
}
private extension PresentationGroupCallState {
static func initialValue(myPeerId: PeerId) -> PresentationGroupCallState {
static func initialValue(myPeerId: PeerId, title: String?) -> PresentationGroupCallState {
return PresentationGroupCallState(
myPeerId: myPeerId,
networkState: .connecting,
@ -204,7 +204,7 @@ private extension PresentationGroupCallState {
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
defaultParticipantMuteState: nil,
recordingStartTimestamp: nil,
title: nil,
title: title,
raisedHand: false
)
}
@ -345,15 +345,16 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
public let internalId: CallSessionInternalId
public let peerId: PeerId
private var joinAsPeerId: PeerId
public let peer: Peer?
public private(set) var isVideo: Bool
private let temporaryJoinTimestamp: Int32
private var internalState: InternalState = .requesting
private let internalStatePromise = Promise<InternalState>(.requesting)
private var callContext: OngoingGroupCallContext?
private var currentConnectionMode: OngoingGroupCallContext.ConnectionMode = .none
private var ssrcMapping: [UInt32: PeerId] = [:]
private var requestedSsrcs = Set<UInt32>()
@ -428,6 +429,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
public var canBeRemoved: Signal<Bool, NoError> {
return self._canBeRemoved.get()
}
private var markedAsCanBeRemoved = false
private let wasRemoved = Promise<Bool>(false)
@ -481,6 +483,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private let isMutedDisposable = MetaDisposable()
private let memberStatesDisposable = MetaDisposable()
private let leaveDisposable = MetaDisposable()
private var isReconnectingAsSpeaker = false {
didSet {
if self.isReconnectingAsSpeaker != oldValue {
self.isReconnectingAsSpeakerPromise.set(self.isReconnectingAsSpeaker)
}
}
}
private let isReconnectingAsSpeakerPromise = ValuePromise<Bool>(false)
private var checkCallDisposable: Disposable?
private var isCurrentlyConnecting: Bool?
@ -515,7 +526,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
initialCall: CachedChannelData.ActiveCall?,
internalId: CallSessionInternalId,
peerId: PeerId,
peer: Peer?,
joinAsPeerId: PeerId?
) {
self.account = accountContext.account
@ -527,10 +537,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.initialCall = initialCall
self.internalId = internalId
self.peerId = peerId
self.peer = peer
self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId
self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId)
self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title)
self.statePromise = ValuePromise(self.stateValue)
self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
@ -654,13 +663,13 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if participantUpdate.peerId == strongSelf.joinAsPeerId {
if case let .established(_, _, _, ssrc, _) = strongSelf.internalState, ssrc == participantUpdate.ssrc {
strongSelf._canBeRemoved.set(.single(true))
strongSelf.markAsCanBeRemoved()
}
}
} else if participantUpdate.peerId == strongSelf.joinAsPeerId {
if case let .established(_, connectionMode, _, ssrc, _) = strongSelf.internalState {
if ssrc != participantUpdate.ssrc {
strongSelf._canBeRemoved.set(.single(true))
strongSelf.markAsCanBeRemoved()
} else if case .broadcast = connectionMode {
let canUnmute: Bool
if let muteState = participantUpdate.muteState {
@ -670,7 +679,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
if canUnmute {
strongSelf.requestCall()
strongSelf.requestCall(movingFromBroadcastToRtc: true)
}
}
}
@ -684,7 +693,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
case let .call(isTerminated, _, _, _):
if isTerminated {
strongSelf._canBeRemoved.set(.single(true))
strongSelf.markAsCanBeRemoved()
}
}
}
@ -734,7 +743,31 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
})
self.requestCall()
let _ = (self.account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in
var canManageCall = false
if let peer = peer as? TelegramGroup {
if case .creator = peer.role {
canManageCall = true
} else if case let .admin(rights, _) = peer.role, rights.rights.contains(.canManageCalls) {
canManageCall = true
}
} else if let peer = peer as? TelegramChannel {
if peer.flags.contains(.isCreator) {
canManageCall = true
} else if (peer.adminRights?.rights.contains(.canManageCalls) == true) {
canManageCall = true
}
}
if let strongSelf = self {
var updatedValue = strongSelf.stateValue
updatedValue.canManageCall = canManageCall
strongSelf.stateValue = updatedValue
}
})
self.requestCall(movingFromBroadcastToRtc: false)
}
deinit {
@ -828,6 +861,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
jsonParams: nil,
joinTimestamp: strongSelf.temporaryJoinTimestamp,
raiseHandRating: nil,
hasRaiseHand: false,
activityTimestamp: nil,
activityRank: nil,
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
@ -880,6 +914,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
let previousInternalState = self.internalState
self.internalState = internalState
self.internalStatePromise.set(.single(internalState))
if let audioSessionControl = audioSessionControl, previousControl == nil {
switch self.currentSelectedAudioOutputValue {
@ -924,7 +959,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
return
}
if case .established = strongSelf.internalState {
strongSelf.requestCall()
strongSelf.requestCall(movingFromBroadcastToRtc: false)
}
}
})
@ -983,10 +1018,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
switch joinCallResult.connectionMode {
case .rtc:
strongSelf.callContext?.setConnectionMode(.rtc)
strongSelf.currentConnectionMode = .rtc
strongSelf.callContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false)
strongSelf.callContext?.setJoinResponse(payload: clientParams, participants: addedParticipants)
case .broadcast:
strongSelf.callContext?.setConnectionMode(.broadcast)
strongSelf.currentConnectionMode = .broadcast
strongSelf.callContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false)
}
strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl)
@ -1006,7 +1043,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
]), on: .root, blockInteraction: false, completion: {})
}
strongSelf._canBeRemoved.set(.single(true))
strongSelf.markAsCanBeRemoved()
}))
}))
@ -1016,12 +1053,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
return
}
let mappedState: PresentationGroupCallState.NetworkState
switch state {
case .connecting:
mappedState = .connecting
case .connected:
if state.isConnected {
mappedState = .connected
} else {
mappedState = .connecting
}
let wasConnecting = strongSelf.stateValue.networkState == .connecting
if strongSelf.stateValue.networkState != mappedState {
strongSelf.stateValue.networkState = mappedState
@ -1037,8 +1074,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.checkCallDisposable = nil
}
}
strongSelf.isReconnectingAsSpeaker = state.isTransitioningFromBroadcastToRtc
if (wasConnecting != isConnecting && strongSelf.didConnectOnce) { //|| !strongSelf.didStartConnectingOnce {
if (wasConnecting != isConnecting && strongSelf.didConnectOnce) {
if isConnecting {
let toneRenderer = PresentationCallToneRenderer(tone: .groupConnecting)
strongSelf.toneRenderer = toneRenderer
@ -1052,7 +1091,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.didStartConnectingOnce = true
}
if case .connected = state {
if state.isConnected {
if !strongSelf.didConnectOnce {
strongSelf.didConnectOnce = true
@ -1179,8 +1218,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.speakingParticipantsContext.get(),
adminIds,
myPeer,
accountContext.account.postbox.peerView(id: peerId)
).start(next: { [weak self] state, activeSpeakers, speakingParticipants, adminIds, myPeer, view in
accountContext.account.postbox.peerView(id: peerId),
self.isReconnectingAsSpeakerPromise.get()
).start(next: { [weak self] state, activeSpeakers, speakingParticipants, adminIds, myPeer, view, isReconnectingAsSpeaker in
guard let strongSelf = self else {
return
}
@ -1227,6 +1267,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
jsonParams: nil,
joinTimestamp: strongSelf.temporaryJoinTimestamp,
raiseHandRating: nil,
hasRaiseHand: false,
activityTimestamp: nil,
activityRank: nil,
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false),
@ -1291,7 +1332,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
})
}
if let muteState = participant.muteState {
var filteredMuteState = participant.muteState
if isReconnectingAsSpeaker || strongSelf.currentConnectionMode != .rtc {
filteredMuteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: false)
}
if let muteState = filteredMuteState {
if muteState.canUnmute {
switch strongSelf.isMutedValue {
case let .muted(isPushToTalkActive):
@ -1341,7 +1387,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp
strongSelf.stateValue.title = state.title
strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState(
participantCount: state.totalCount,
topParticipants: topParticipants,
@ -1473,7 +1519,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
return
}
strongSelf.checkCallDisposable = nil
strongSelf.requestCall()
strongSelf.requestCall(movingFromBroadcastToRtc: false)
})
}
}
@ -1486,6 +1532,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
private func markAsCanBeRemoved() {
if self.markedAsCanBeRemoved {
return
}
self.markedAsCanBeRemoved = true
self.callContext?.stop()
self._canBeRemoved.set(.single(true))
@ -1535,7 +1586,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.stateValue.myPeerId = peerId
}
strongSelf.requestCall()
strongSelf.requestCall(movingFromBroadcastToRtc: false)
})
}
@ -1784,14 +1835,16 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.participantsContext?.updateShouldBeRecording(shouldBeRecording, title: title)
}
private func requestCall() {
self.callContext?.setConnectionMode(.none)
private func requestCall(movingFromBroadcastToRtc: Bool) {
self.currentConnectionMode = .none
self.callContext?.setConnectionMode(.none, keepBroadcastConnectedIfWasEnabled: movingFromBroadcastToRtc)
self.missingSsrcsDisposable.set(nil)
self.missingSsrcs.removeAll()
self.processedMissingSsrcs.removeAll()
self.internalState = .requesting
self.internalStatePromise.set(.single(.requesting))
self.isCurrentlyConnecting = nil
enum CallError {
@ -1829,7 +1882,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.checkCallDisposable = nil
self.stateValue.networkState = .connecting
self.stateValue.title = initialCall?.title
if !movingFromBroadcastToRtc {
self.stateValue.networkState = .connecting
}
self.requestDisposable.set((currentOrRequestedCall
|> deliverOnMainQueue).start(next: { [weak self] value in
@ -1842,7 +1898,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.updateSessionState(internalState: .active(value), audioSessionControl: strongSelf.audioSessionControl)
} else {
strongSelf._canBeRemoved.set(.single(true))
strongSelf.markAsCanBeRemoved()
}
}))
}
@ -1856,7 +1912,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
updatedInvitedPeers.insert(peerId, at: 0)
self.invitedPeersValue = updatedInvitedPeers
let _ = inviteToGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId, canUnmute: false).start()
let _ = inviteToGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start()
return true
}
@ -1875,6 +1931,31 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
let _ = editGroupCallTitle(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, title: title).start()
}
public var inviteLinks: Signal<GroupCallInviteLinks?, NoError> {
return self.state
|> map { state -> PeerId in
return state.myPeerId
}
|> distinctUntilChanged
|> mapToSignal { _ -> Signal<GroupCallInviteLinks?, NoError> in
return self.internalStatePromise.get()
|> filter { state -> Bool in
if case .requesting = state {
return false
} else {
return true
}
}
|> mapToSignal { state in
if let callInfo = state.callInfo {
return groupCallInviteLinks(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash)
} else {
return .complete()
}
}
}
}
private var currentMyAudioLevel: Float = 0.0
private var currentMyAudioLevelTimestamp: Double = 0.0
private var isSendingTyping: Bool = false

View File

@ -727,10 +727,10 @@ public final class VoiceChatController: ViewController {
self.topPanelEdgeNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
}
self.optionsButton = VoiceChatHeaderButton()
self.optionsButton.setImage(optionsButtonImage(dark: false))
self.closeButton = VoiceChatHeaderButton()
self.closeButton.setImage(closeButtonImage(dark: false))
self.optionsButton = VoiceChatHeaderButton(context: context)
self.optionsButton.setContent(.image(optionsButtonImage(dark: false)))
self.closeButton = VoiceChatHeaderButton(context: context)
self.closeButton.setContent(.image(closeButtonImage(dark: false)))
self.titleNode = VoiceChatControllerTitleNode(theme: self.presentationData.theme)
self.titleNode.isUserInteractionEnabled = false
@ -782,7 +782,7 @@ public final class VoiceChatController: ViewController {
let displayAsPeers: Signal<[FoundPeer], NoError> = currentAccountPeer
|> then(
combineLatest(currentAccountPeer, groupCallDisplayAsAvailablePeers(network: context.account.network, postbox: context.account.postbox, peerId: call.peerId))
combineLatest(currentAccountPeer, cachedGroupCallDisplayAsAvailablePeers(account: context.account, peerId: call.peerId))
|> map { currentAccountPeer, availablePeers -> [FoundPeer] in
var result = currentAccountPeer
result.append(contentsOf: availablePeers)
@ -874,7 +874,14 @@ public final class VoiceChatController: ViewController {
strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: participant.peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), action: { _ in return false })
}
} else {
strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: {
let text: String
if let peer = groupPeer as? TelegramChannel, case .broadcast = peer.info {
text = strongSelf.presentationData.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0
} else {
text = strongSelf.presentationData.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0
}
strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: {
guard let strongSelf = self else {
return
}
@ -1438,6 +1445,10 @@ public final class VoiceChatController: ViewController {
self.cameraButtonNode.addTarget(self, action: #selector(self.cameraPressed), forControlEvents: .touchUpInside)
let inviteLinksPromise = Promise<GroupCallInviteLinks?>(nil)
inviteLinksPromise.set(.single(nil)
|> then(call.inviteLinks))
let avatarSize = CGSize(width: 28.0, height: 28.0)
self.optionsButton.contextAction = { [weak self] sourceNode, gesture in
guard let strongSelf = self, let controller = strongSelf.controller else {
@ -1496,9 +1507,11 @@ public final class VoiceChatController: ViewController {
return
}
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 != 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 {
@ -1563,9 +1576,9 @@ public final class VoiceChatController: ViewController {
}
mainItemsImpl = {
return combineLatest(displayAsPeersPromise.get(), context.account.postbox.loadedPeerWithId(call.peerId))
return combineLatest(displayAsPeersPromise.get(), context.account.postbox.loadedPeerWithId(call.peerId), inviteLinksPromise.get())
|> take(1)
|> map { peers, chatPeer -> [ContextMenuItem] in
|> map { peers, chatPeer, inviteLinks -> [ContextMenuItem] in
let presentationData = strongSelf.presentationData
var items: [ContextMenuItem] = []
@ -1610,52 +1623,46 @@ public final class VoiceChatController: ViewController {
})))
}
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)
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.account.postbox.transaction { transaction -> String? in
if let peer = transaction.getPeer(call.peerId), let addressName = peer.addressName, !addressName.isEmpty {
return "https://t.me/\(addressName)"
} else if let cachedData = transaction.getPeerCachedData(peerId: call.peerId) {
if let cachedData = cachedData as? CachedChannelData {
return cachedData.exportedInvitation?.link
} else if let cachedData = cachedData as? CachedGroupData {
return cachedData.exportedInvitation?.link
}
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)
guard let strongSelf = self else {
return
}
return nil
} |> deliverOnMainQueue).start(next: { link in
if let link = link {
if let strongSelf = self {
let formatSendTitle: (String) -> String = { string in
var string = string
if string.contains("[") && string.contains("]") {
if let startIndex = string.firstIndex(of: "["), let endIndex = string.firstIndex(of: "]") {
string.removeSubrange(startIndex ... endIndex)
}
} else {
string = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,."))
}
return string
let formatSendTitle: (String) -> String = { string in
var string = string
if string.contains("[") && string.contains("]") {
if let startIndex = string.firstIndex(of: "["), let endIndex = string.firstIndex(of: "]") {
string.removeSubrange(startIndex ... endIndex)
}
let segmentedValues = [ShareControllerSegmentedValue(title: strongSelf.presentationData.strings.VoiceChat_InviteLink_Speaker, subject: .url(link), actionTitle: strongSelf.presentationData.strings.VoiceChat_InviteLink_CopySpeakerLink, formatSendTitle: { count in
return formatSendTitle(strongSelf.presentationData.strings.VoiceChat_InviteLink_InviteSpeakers(Int32(count)))
}), ShareControllerSegmentedValue(title: strongSelf.presentationData.strings.VoiceChat_InviteLink_Listener, subject: .url(link), actionTitle: strongSelf.presentationData.strings.VoiceChat_InviteLink_CopyListenerLink, formatSendTitle: { count in
return formatSendTitle(strongSelf.presentationData.strings.VoiceChat_InviteLink_InviteListeners(Int32(count)))
})]
let shareController = ShareController(context: strongSelf.context, subject: .url(link), segmentedValues: segmentedValues, forcedTheme: strongSelf.darkTheme, forcedActionTitle: strongSelf.presentationData.strings.VoiceChat_CopyInviteLink)
strongSelf.controller?.present(shareController, in: .window(.root))
} else {
string = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,."))
}
return string
}
var segmentedValues: [ShareControllerSegmentedValue]?
if let speakerLink = inviteLinks.speakerLink {
segmentedValues = [ShareControllerSegmentedValue(title: strongSelf.presentationData.strings.VoiceChat_InviteLink_Speaker, subject: .url(speakerLink), actionTitle: strongSelf.presentationData.strings.VoiceChat_InviteLink_CopySpeakerLink, formatSendTitle: { count in
return formatSendTitle(strongSelf.presentationData.strings.VoiceChat_InviteLink_InviteSpeakers(Int32(count)))
}), ShareControllerSegmentedValue(title: strongSelf.presentationData.strings.VoiceChat_InviteLink_Listener, subject: .url(inviteLinks.listenerLink), actionTitle: strongSelf.presentationData.strings.VoiceChat_InviteLink_CopyListenerLink, formatSendTitle: { count in
return formatSendTitle(strongSelf.presentationData.strings.VoiceChat_InviteLink_InviteListeners(Int32(count)))
})]
}
let shareController = ShareController(context: strongSelf.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forcedTheme: strongSelf.darkTheme, forcedActionTitle: strongSelf.presentationData.strings.VoiceChat_InviteLink_CopyListenerLink)
shareController.actionCompleted = { [weak self] in
if let strongSelf = self {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}
}
})
})))
strongSelf.controller?.present(shareController, in: .window(.root))
})))
}
if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp {
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { _, f in
@ -2259,8 +2266,8 @@ public final class VoiceChatController: ViewController {
}
self.bottomCornersNode.image = cornersImage(top: false, bottom: true, dark: isFullscreen)
self.optionsButton.setImage(optionsButtonImage(dark: isFullscreen), animated: transition.isAnimated)
self.closeButton.setImage(closeButtonImage(dark: isFullscreen), animated: transition.isAnimated)
self.optionsButton.setContent(.image(optionsButtonImage(dark: isFullscreen)), animated: transition.isAnimated)
self.closeButton.setContent(.image(closeButtonImage(dark: isFullscreen)), animated: transition.isAnimated)
self.updateTitle(transition: transition)
}
@ -2737,10 +2744,12 @@ public final class VoiceChatController: ViewController {
var processedPeerIds = Set<PeerId>()
var canInvite = true
if let peer = self.peer as? TelegramChannel, peer.flags.contains(.isGigagroup) {
if peer.flags.contains(.isCreator) || peer.adminRights != nil {
} else {
canInvite = false
if let peer = self.peer as? TelegramChannel {
if peer.flags.contains(.isGigagroup) || (peer.addressName?.isEmpty ?? true) {
if peer.flags.contains(.isCreator) || peer.adminRights != nil {
} else {
canInvite = false
}
}
}
if canInvite {
@ -2755,7 +2764,7 @@ public final class VoiceChatController: ViewController {
let memberState: PeerEntry.State
var memberMuteState: GroupCallParticipantsContext.Participant.MuteState?
if member.raiseHandRating != nil {
if member.raiseHandRating != nil || member.hasRaiseHand {
memberState = .raisedHand
memberMuteState = member.muteState
} else if member.peer.id == self.callState?.myPeerId {

View File

@ -2,6 +2,10 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import AccountContext
import TelegramPresentationData
import AvatarNode
func optionsBackgroundImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
@ -45,13 +49,25 @@ func closeButtonImage(dark: Bool) -> UIImage? {
}
final class VoiceChatHeaderButton: HighlightableButtonNode {
enum Content {
case image(UIImage?)
case avatar(Peer)
}
private let context: AccountContext
private var theme: PresentationTheme
let referenceNode: ContextReferenceContentNode
let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
private let avatarNode: AvatarNode
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
init() {
init(context: AccountContext) {
self.context = context
self.theme = context.sharedContext.currentPresentationData.with { $0 }.theme
self.referenceNode = ContextReferenceContentNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.animateScale = false
@ -60,10 +76,14 @@ final class VoiceChatHeaderButton: HighlightableButtonNode {
self.iconNode.displayWithoutProcessing = true
self.iconNode.contentMode = .scaleToFill
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0))
self.avatarNode.isHidden = true
super.init()
self.containerNode.addSubnode(self.referenceNode)
self.referenceNode.addSubnode(self.iconNode)
self.referenceNode.addSubnode(self.avatarNode)
self.addSubnode(self.containerNode)
self.containerNode.shouldBegin = { [weak self] location in
@ -84,20 +104,46 @@ final class VoiceChatHeaderButton: HighlightableButtonNode {
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0))
self.referenceNode.frame = self.containerNode.bounds
self.iconNode.frame = self.containerNode.bounds
self.avatarNode.frame = self.containerNode.bounds
}
func setImage(_ image: UIImage?, animated: Bool = false) {
if animated, let snapshotView = self.iconNode.view.snapshotContentTree() {
snapshotView.frame = self.iconNode.frame
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
private var content: Content?
func setContent(_ content: Content, animated: Bool = false) {
if animated {
switch content {
case let .image(image):
if let snapshotView = self.referenceNode.view.snapshotContentTree() {
snapshotView.frame = self.referenceNode.frame
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.avatarNode.isHidden = true
case let .avatar(peer):
self.avatarNode.setPeer(context: self.context, theme: self.theme, peer: peer)
self.iconNode.isHidden = true
self.avatarNode.isHidden = false
}
} else {
self.content = content
switch content {
case let .image(image):
self.iconNode.image = image
self.iconNode.isHidden = false
self.avatarNode.isHidden = true
case let .avatar(peer):
self.avatarNode.setPeer(context: self.context, theme: self.theme, peer: peer)
self.iconNode.isHidden = true
self.avatarNode.isHidden = false
}
}
self.iconNode.image = image
}
override func didLoad() {
super.didLoad()
self.view.isOpaque = false

View File

@ -1,6 +1,7 @@
import Foundation
import Postbox
import SwiftSignalKit
import SyncCore
import TelegramApi
import MtProtoKit
@ -32,6 +33,8 @@ private func createChannel(account: Account, title: String, description: String?
address = location.address
}
transaction.clearItemCacheCollection(collectionId: Namespaces.CachedItemCollection.cachedGroupCallDisplayAsPeers)
return account.network.request(Api.functions.channels.createChannel(flags: flags, title: title, about: description ?? "", geoPoint: geoPoint, address: address), automaticFloodWait: false)
|> mapError { error -> CreateChannelError in
if error.errorCode == 406 {

View File

@ -40,7 +40,7 @@ public struct GroupCallSummary: Equatable {
extension GroupCallInfo {
init?(_ call: Api.GroupCall) {
switch call {
case let .groupCall(_, id, accessHash, participantCount, params, title, streamDcId, recordStartDate, _):
case let .groupCall(flags, id, accessHash, participantCount, params, title, streamDcId, recordStartDate, _):
var clientParams: String?
if let params = params {
switch params {
@ -143,6 +143,7 @@ public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int
jsonParams: jsonParams,
joinTimestamp: date,
raiseHandRating: raiseHandRating,
hasRaiseHand: raiseHandRating != nil,
activityTimestamp: activeDate.flatMap(Double.init),
activityRank: nil,
muteState: muteState,
@ -309,6 +310,7 @@ public func getGroupCallParticipants(account: Account, callId: Int64, accessHash
jsonParams: jsonParams,
joinTimestamp: date,
raiseHandRating: raiseHandRating,
hasRaiseHand: raiseHandRating != nil,
activityTimestamp: activeDate.flatMap(Double.init),
activityRank: nil,
muteState: muteState,
@ -337,29 +339,6 @@ public func getGroupCallParticipants(account: Account, callId: Int64, accessHash
}
}
public func inviteToGroupCall(account: Account, peerId: PeerId, callId: Int64, accessHash: Int64, users: Set<PeerId>, canUnmute: Bool) -> Signal<Void, NoError> {
return .complete()
// return account.postbox.transaction { transaction -> [Api.InputUser] in
// var inputUsers: [Api.InputUser] = []
// for user in users {
// if let peer = transaction.getPeer(user), let inputUser = apiInputUser(peer) {
// inputUsers.append(inputUser)
// }
// }
// return inputUsers
// }
// |> mapToSignal { users -> Signal<Void, NoError> in
// return account.network.request(Api.functions.phone.inviteToGroupCall(flags: 0, call: .inputGroupCall(id: callId, accessHash: accessHash), users: users))
// |> `catch` { _ -> Signal<Void, NoError> in
// return .single(Void())
// } |> mapToSignal { updates -> Signal<Void, NoError> in
// account.stateManager.addUpdates(updates)
//
// return .single(Void())
// }
// }
}
public enum JoinGroupCallError {
case generic
case anonymousNotAllowed
@ -714,6 +693,7 @@ public final class GroupCallParticipantsContext {
public var jsonParams: String?
public var joinTimestamp: Int32
public var raiseHandRating: Int64?
public var hasRaiseHand: Bool
public var activityTimestamp: Double?
public var activityRank: Int?
public var muteState: MuteState?
@ -726,6 +706,7 @@ public final class GroupCallParticipantsContext {
jsonParams: String?,
joinTimestamp: Int32,
raiseHandRating: Int64?,
hasRaiseHand: Bool,
activityTimestamp: Double?,
activityRank: Int?,
muteState: MuteState?,
@ -737,6 +718,7 @@ public final class GroupCallParticipantsContext {
self.jsonParams = jsonParams
self.joinTimestamp = joinTimestamp
self.raiseHandRating = raiseHandRating
self.hasRaiseHand = hasRaiseHand
self.activityTimestamp = activityTimestamp
self.activityRank = activityRank
self.muteState = muteState
@ -762,6 +744,9 @@ public final class GroupCallParticipantsContext {
if lhs.raiseHandRating != rhs.raiseHandRating {
return false
}
if lhs.hasRaiseHand != rhs.hasRaiseHand {
return false
}
if lhs.activityTimestamp != rhs.activityTimestamp {
return false
}
@ -973,17 +958,27 @@ public final class GroupCallParticipantsContext {
public var immediateState: State?
public var state: Signal<State, NoError> {
let accountPeerId = self.account.peerId
return self.statePromise.get()
|> map { state -> State in
if state.overlayState.isEmpty {
return state.state
}
var publicState = state.state
var sortAgain = false
let canSeeHands = state.state.isCreator || state.state.adminIds.contains(accountPeerId)
for i in 0 ..< publicState.participants.count {
if let pendingMuteState = state.overlayState.pendingMuteStateChanges[publicState.participants[i].peer.id] {
publicState.participants[i].muteState = pendingMuteState.state
publicState.participants[i].volume = pendingMuteState.volume
}
if !canSeeHands && publicState.participants[i].raiseHandRating != nil {
publicState.participants[i].raiseHandRating = nil
sortAgain = true
}
}
if sortAgain {
publicState.participants.sort()
}
return publicState
}
@ -1389,6 +1384,7 @@ public final class GroupCallParticipantsContext {
jsonParams: participantUpdate.jsonParams,
joinTimestamp: participantUpdate.joinTimestamp,
raiseHandRating: participantUpdate.raiseHandRating,
hasRaiseHand: participantUpdate.raiseHandRating != nil,
activityTimestamp: activityTimestamp,
activityRank: previousActivityRank,
muteState: participantUpdate.muteState,
@ -1755,7 +1751,7 @@ public enum InviteToGroupCallError {
case generic
}
public func inviteToGroupCall(account: Account, callId: Int64, accessHash: Int64, peerId: PeerId, canUnmute: Bool) -> Signal<Never, InviteToGroupCallError> {
public func inviteToGroupCall(account: Account, callId: Int64, accessHash: Int64, peerId: PeerId) -> Signal<Never, InviteToGroupCallError> {
return account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
@ -1767,12 +1763,8 @@ public func inviteToGroupCall(account: Account, callId: Int64, accessHash: Int64
guard let apiUser = apiInputUser(user) else {
return .fail(.generic)
}
var flags: Int32 = 0
if canUnmute {
flags |= (1 << 0)
}
return account.network.request(Api.functions.phone.inviteToGroupCall(flags: flags, call: .inputGroupCall(id: callId, accessHash: accessHash), users: [apiUser]))
return account.network.request(Api.functions.phone.inviteToGroupCall(call: .inputGroupCall(id: callId, accessHash: accessHash), users: [apiUser]))
|> mapError { _ -> InviteToGroupCallError in
return .generic
}
@ -1784,6 +1776,47 @@ public func inviteToGroupCall(account: Account, callId: Int64, accessHash: Int64
}
}
public struct GroupCallInviteLinks {
public let listenerLink: String
public let speakerLink: String?
}
public func groupCallInviteLinks(account: Account, callId: Int64, accessHash: Int64) -> Signal<GroupCallInviteLinks?, NoError> {
let call = Api.InputGroupCall.inputGroupCall(id: callId, accessHash: accessHash)
let listenerInvite: Signal<String?, NoError> = account.network.request(Api.functions.phone.exportGroupCallInvite(flags: 0, call: call))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.phone.ExportedGroupCallInvite?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<String?, NoError> in
if let result = result, case let .exportedGroupCallInvite(link) = result {
return .single(link)
}
return .single(nil)
}
let speakerInvite: Signal<String?, NoError> = account.network.request(Api.functions.phone.exportGroupCallInvite(flags: 1 << 0, call: call))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.phone.ExportedGroupCallInvite?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<String?, NoError> in
if let result = result, case let .exportedGroupCallInvite(link) = result {
return .single(link)
}
return .single(nil)
}
return combineLatest(listenerInvite, speakerInvite)
|> map { listenerLink, speakerLink in
if let listenerLink = listenerLink {
return GroupCallInviteLinks(listenerLink: listenerLink, speakerLink: speakerLink)
} else {
return nil
}
}
}
public enum EditGroupCallTitleError {
case generic
}
@ -1861,6 +1894,7 @@ public final class CachedDisplayAsPeers: PostboxCoding {
}
}
public func cachedGroupCallDisplayAsAvailablePeers(account: Account, peerId: PeerId) -> Signal<[FoundPeer], NoError> {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
@ -1884,14 +1918,14 @@ public func cachedGroupCallDisplayAsAvailablePeers(account: Account, peerId: Pee
}
|> mapToSignal { cachedPeersAndTimestamp -> Signal<[FoundPeer], NoError> in
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let (cachedPeers, timestamp) = cachedPeersAndTimestamp, currentTimestamp - timestamp < 60 * 5 && !cachedPeers.isEmpty {
if let (cachedPeers, timestamp) = cachedPeersAndTimestamp, currentTimestamp - timestamp < 60 * 3 && !cachedPeers.isEmpty {
return .single(cachedPeers)
} else {
return groupCallDisplayAsAvailablePeers(network: account.network, postbox: account.postbox, peerId: peerId)
|> mapToSignal { peers -> Signal<[FoundPeer], NoError> in
return account.postbox.transaction { transaction -> [FoundPeer] in
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedGroupCallDisplayAsPeers, key: key), entry: CachedDisplayAsPeers(peerIds: peers.map { $0.peer.id }, timestamp: currentTimestamp), collectionSpec: ItemCacheCollectionSpec(lowWaterItemCount: 1, highWaterItemCount: 1))
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedGroupCallDisplayAsPeers, key: key), entry: CachedDisplayAsPeers(peerIds: peers.map { $0.peer.id }, timestamp: currentTimestamp), collectionSpec: ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 20))
return peers
}
}

View File

@ -11865,7 +11865,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return [FoundPeer(peer: peer, subscribers: nil)]
}
let _ = (combineLatest(currentAccountPeer, groupCallDisplayAsAvailablePeers(network: context.account.network, postbox: context.account.postbox, peerId: peerId))
let _ = (combineLatest(currentAccountPeer, cachedGroupCallDisplayAsAvailablePeers(account: context.account, peerId: peerId))
|> map { currentAccountPeer, availablePeers -> [FoundPeer] in
var result = currentAccountPeer
result.append(contentsOf: availablePeers)

View File

@ -279,6 +279,8 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
actionTitle = item.presentationData.strings.Conversation_ViewGroup
case "telegram_message":
actionTitle = item.presentationData.strings.Conversation_ViewMessage
case "telegram_voicechat":
actionTitle = item.presentationData.strings.Conversation_JoinVoiceChat
case "telegram_background":
title = item.presentationData.strings.Conversation_ChatBackground
subtitle = nil

View File

@ -1742,7 +1742,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}
index -= 1
}
viewControllers.remove(atOffsets: IndexSet(indexesToRemove))
for i in indexesToRemove.sorted().reversed() {
viewControllers.remove(at: i)
}
navigationController.setViewControllers(viewControllers, animated: false)
}))
}
@ -1882,7 +1884,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}
index -= 1
}
viewControllers.remove(atOffsets: IndexSet(indexesToRemove))
for i in indexesToRemove.sorted().reversed() {
viewControllers.remove(at: i)
}
navigationController.setViewControllers(viewControllers, animated: false)
}))
}
@ -2780,7 +2784,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peerId.namespace) {
self.displayAsPeersPromise.set(groupCallDisplayAsAvailablePeers(network: context.account.network, postbox: context.account.postbox, peerId: peerId))
self.displayAsPeersPromise.set(cachedGroupCallDisplayAsAvailablePeers(account: context.account, peerId: peerId))
}
}

View File

@ -134,9 +134,9 @@ public final class OngoingGroupCallContext {
case broadcast
}
public enum NetworkState {
case connecting
case connected
public struct NetworkState: Equatable {
public var isConnected: Bool
public var isTransitioningFromBroadcastToRtc: Bool
}
public enum AudioLevelKey: Hashable {
@ -151,7 +151,7 @@ public final class OngoingGroupCallContext {
let sessionId = UInt32.random(in: 0 ..< UInt32(Int32.max))
let joinPayload = Promise<(String, UInt32)>()
let networkState = ValuePromise<NetworkState>(.connecting, ignoreRepeated: true)
let networkState = ValuePromise<NetworkState>(NetworkState(isConnected: false, isTransitioningFromBroadcastToRtc: false), ignoreRepeated: true)
let isMuted = ValuePromise<Bool>(true, ignoreRepeated: true)
let audioLevels = ValuePipe<[(AudioLevelKey, Float, Bool)]>()
@ -210,16 +210,7 @@ public final class OngoingGroupCallContext {
guard let strongSelf = self else {
return
}
let mappedState: NetworkState
switch state {
case .connecting:
mappedState = .connecting
case .connected:
mappedState = .connected
@unknown default:
mappedState = .connecting
}
strongSelf.networkState.set(mappedState)
strongSelf.networkState.set(NetworkState(isConnected: state.isConnected, isTransitioningFromBroadcastToRtc: state.isTransitioningFromBroadcastToRtc))
}
}
@ -295,7 +286,7 @@ public final class OngoingGroupCallContext {
self.context.stop()
}
func setConnectionMode(_ connectionMode: ConnectionMode) {
func setConnectionMode(_ connectionMode: ConnectionMode, keepBroadcastConnectedIfWasEnabled: Bool) {
let mappedConnectionMode: OngoingCallConnectionMode
switch connectionMode {
case .none:
@ -305,7 +296,7 @@ public final class OngoingGroupCallContext {
case .broadcast:
mappedConnectionMode = .broadcast
}
self.context.setConnectionMode(mappedConnectionMode)
self.context.setConnectionMode(mappedConnectionMode, keepBroadcastConnectedIfWasEnabled: keepBroadcastConnectedIfWasEnabled)
if (mappedConnectionMode != .rtc) {
self.joinPayload.set(.never())
@ -504,9 +495,9 @@ public final class OngoingGroupCallContext {
})
}
public func setConnectionMode(_ connectionMode: ConnectionMode) {
public func setConnectionMode(_ connectionMode: ConnectionMode, keepBroadcastConnectedIfWasEnabled: Bool) {
self.impl.with { impl in
impl.setConnectionMode(connectionMode)
impl.setConnectionMode(connectionMode, keepBroadcastConnectedIfWasEnabled: keepBroadcastConnectedIfWasEnabled)
}
}

View File

@ -151,10 +151,10 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
- (void)switchAudioInput:(NSString * _Nonnull)deviceId;
@end
typedef NS_ENUM(int32_t, GroupCallNetworkState) {
GroupCallNetworkStateConnecting,
GroupCallNetworkStateConnected
};
typedef struct {
bool isConnected;
bool isTransitioningFromBroadcastToRtc;
} GroupCallNetworkState;
@interface OngoingGroupCallParticipantDescription : NSObject
@ -200,7 +200,7 @@ typedef NS_ENUM(int32_t, OngoingGroupCallBroadcastPartStatus) {
- (void)stop;
- (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode;
- (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode keepBroadcastConnectedIfWasEnabled:(bool)keepBroadcastConnectedIfWasEnabled;
- (void)emitJoinPayload:(void (^ _Nonnull)(NSString * _Nonnull, uint32_t))completion;
- (void)setJoinResponsePayload:(NSString * _Nonnull)payload participants:(NSArray<OngoingGroupCallParticipantDescription *> * _Nonnull)participants;

View File

@ -863,13 +863,16 @@ private:
__weak GroupCallThreadLocalContext *weakSelf = self;
_instance.reset(new tgcalls::GroupInstanceCustomImpl((tgcalls::GroupInstanceDescriptor){
.networkStateUpdated = [weakSelf, queue, networkStateUpdated](bool isConnected) {
.networkStateUpdated = [weakSelf, queue, networkStateUpdated](tgcalls::GroupNetworkState networkState) {
[queue dispatch:^{
__strong GroupCallThreadLocalContext *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
networkStateUpdated(isConnected ? GroupCallNetworkStateConnected : GroupCallNetworkStateConnecting);
GroupCallNetworkState mappedState;
mappedState.isConnected = networkState.isConnected;
mappedState.isTransitioningFromBroadcastToRtc = networkState.isTransitioningFromBroadcastToRtc;
networkStateUpdated(mappedState);
}];
},
.audioLevelsUpdated = [audioLevelsUpdated](tgcalls::GroupLevelsUpdate const &levels) {
@ -1041,7 +1044,7 @@ static void processJoinPayload(tgcalls::GroupJoinPayload &payload, void (^ _Nonn
completion(string, payload.ssrc);
}
- (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode {
- (void)setConnectionMode:(OngoingCallConnectionMode)connectionMode keepBroadcastConnectedIfWasEnabled:(bool)keepBroadcastConnectedIfWasEnabled {
if (_instance) {
tgcalls::GroupConnectionMode mappedConnectionMode;
switch (connectionMode) {
@ -1062,7 +1065,7 @@ static void processJoinPayload(tgcalls::GroupJoinPayload &payload, void (^ _Nonn
break;
}
}
_instance->setConnectionMode(mappedConnectionMode);
_instance->setConnectionMode(mappedConnectionMode, keepBroadcastConnectedIfWasEnabled);
}
}

@ -1 +1 @@
Subproject commit 2c55abafe21cf67128dc3733844e37276fbcabb3
Subproject commit d19c74b1474e4aab01d797373f1d62e4da5f87a5

View File

@ -1,5 +1,5 @@
{
"app": "7.5.1",
"app": "7.6",
"bazel": "4.0.0",
"xcode": "12.4"
}