Voice chat UI changes

This commit is contained in:
Ali 2020-12-04 01:34:16 +00:00
parent dc4eed539e
commit b08e5eb0cf
24 changed files with 5097 additions and 4539 deletions

View File

@ -5919,6 +5919,7 @@ Sorry for the inconvenience.";
"VoiceChat.StatusSpeaking" = "speaking"; "VoiceChat.StatusSpeaking" = "speaking";
"VoiceChat.StatusListening" = "listening"; "VoiceChat.StatusListening" = "listening";
"VoiceChat.StatusInvited" = "invited";
"VoiceChat.Connecting" = "Connecting..."; "VoiceChat.Connecting" = "Connecting...";
"VoiceChat.Reconnecting" = "Reconnecting..."; "VoiceChat.Reconnecting" = "Reconnecting...";
@ -5949,6 +5950,7 @@ Sorry for the inconvenience.";
"VoiceChat.PanelJoin" = "Join"; "VoiceChat.PanelJoin" = "Join";
"VoiceChat.Title" = "Voice Chat"; "VoiceChat.Title" = "Voice Chat";
"VoiceChat.InviteMember" = "Invite Member";
"VoiceChat.UserInvited" = "You invited **%@** to the voice chat"; "VoiceChat.UserInvited" = "You invited **%@** to the voice chat";
"Notification.VoiceChatInvitation" = "%1$@ invited %2$@ to the voice chat"; "Notification.VoiceChatInvitation" = "%1$@ invited %2$@ to the voice chat";
@ -5974,6 +5976,7 @@ Sorry for the inconvenience.";
"ChannelInfo.CreateVoiceChat" = "Start Voice Chat"; "ChannelInfo.CreateVoiceChat" = "Start Voice Chat";
"VoiceChat.AnonymousDisabledAlertText" = "Sorry, you can't join voice chat as an anonymous admin."; "VoiceChat.AnonymousDisabledAlertText" = "Sorry, you can't join voice chat as an anonymous admin.";
"VoiceChat.ChatFullAlertText" = "Sorry, this voice chat has too many participants at the moment.";
"VoiceChat.EndConfirmationTitle" = "End voice chat"; "VoiceChat.EndConfirmationTitle" = "End voice chat";
"VoiceChat.EndConfirmationText" = "Are you sure you want to end this voice chat?"; "VoiceChat.EndConfirmationText" = "Are you sure you want to end this voice chat?";

View File

@ -12,10 +12,6 @@ import Display
import DeviceLocationManager import DeviceLocationManager
import TemporaryCachedPeerDataManager import TemporaryCachedPeerDataManager
#if ENABLE_WALLET
import WalletCore
#endif
public final class TelegramApplicationOpenUrlCompletion { public final class TelegramApplicationOpenUrlCompletion {
public let completion: (Bool) -> Void public let completion: (Bool) -> Void
@ -647,14 +643,16 @@ public final class TonContext {
public protocol ChatLocationContextHolder: class { public protocol ChatLocationContextHolder: class {
} }
public protocol AccountGroupCallContext: class {
}
public protocol AccountGroupCallContextCache: class {
}
public protocol AccountContext: class { public protocol AccountContext: class {
var sharedContext: SharedAccountContext { get } var sharedContext: SharedAccountContext { get }
var account: Account { get } var account: Account { get }
#if ENABLE_WALLET
var tonContext: StoredTonContext? { get }
#endif
var liveLocationManager: LiveLocationManager? { get } var liveLocationManager: LiveLocationManager? { get }
var peersNearbyManager: PeersNearbyManager? { get } var peersNearbyManager: PeersNearbyManager? { get }
var fetchManager: FetchManager { get } var fetchManager: FetchManager { get }
@ -663,15 +661,12 @@ public protocol AccountContext: class {
var wallpaperUploadManager: WallpaperUploadManager? { get } var wallpaperUploadManager: WallpaperUploadManager? { get }
var watchManager: WatchManager? { get } var watchManager: WatchManager? { get }
#if ENABLE_WALLET
var hasWallets: Signal<Bool, NoError> { get }
var hasWalletAccess: Signal<Bool, NoError> { get }
#endif
var currentLimitsConfiguration: Atomic<LimitsConfiguration> { get } var currentLimitsConfiguration: Atomic<LimitsConfiguration> { get }
var currentContentSettings: Atomic<ContentSettings> { get } var currentContentSettings: Atomic<ContentSettings> { get }
var currentAppConfiguration: Atomic<AppConfiguration> { get } var currentAppConfiguration: Atomic<AppConfiguration> { get }
var cachedGroupCallContexts: AccountGroupCallContextCache { get }
func storeSecureIdPassword(password: String) func storeSecureIdPassword(password: String)
func getStoredSecureIdPassword() -> String? func getStoredSecureIdPassword() -> String?

View File

@ -194,20 +194,20 @@ public struct PresentationGroupCallSummaryState: Equatable {
public var participantCount: Int public var participantCount: Int
public var callState: PresentationGroupCallState public var callState: PresentationGroupCallState
public var topParticipants: [GroupCallParticipantsContext.Participant] public var topParticipants: [GroupCallParticipantsContext.Participant]
public var numberOfActiveSpeakers: Int public var activeSpeakers: Set<PeerId>
public init( public init(
info: GroupCallInfo, info: GroupCallInfo,
participantCount: Int, participantCount: Int,
callState: PresentationGroupCallState, callState: PresentationGroupCallState,
topParticipants: [GroupCallParticipantsContext.Participant], topParticipants: [GroupCallParticipantsContext.Participant],
numberOfActiveSpeakers: Int activeSpeakers: Set<PeerId>
) { ) {
self.info = info self.info = info
self.participantCount = participantCount self.participantCount = participantCount
self.callState = callState self.callState = callState
self.topParticipants = topParticipants self.topParticipants = topParticipants
self.numberOfActiveSpeakers = numberOfActiveSpeakers self.activeSpeakers = activeSpeakers
} }
} }
@ -286,7 +286,7 @@ public protocol PresentationGroupCall: class {
func updateMuteState(peerId: PeerId, isMuted: Bool) func updateMuteState(peerId: PeerId, isMuted: Bool)
func invitePeer(_ peerId: PeerId) func invitePeer(_ peerId: PeerId)
var invitedPeers: Signal<Set<PeerId>, NoError> { get } var invitedPeers: Signal<[PeerId], NoError> { get }
var sourcePanel: ASDisplayNode? { get set } var sourcePanel: ASDisplayNode? { get set }
} }

View File

@ -15,6 +15,7 @@ swift_library(
"//submodules/SyncCore:SyncCore", "//submodules/SyncCore:SyncCore",
"//submodules/AccountContext:AccountContext", "//submodules/AccountContext:AccountContext",
"//submodules/AvatarNode:AvatarNode", "//submodules/AvatarNode:AvatarNode",
"//submodules/AudioBlob:AudioBlob",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -8,6 +8,7 @@ import Postbox
import TelegramCore import TelegramCore
import SyncCore import SyncCore
import AccountContext import AccountContext
import AudioBlob
public final class AnimatedAvatarSetContext { public final class AnimatedAvatarSetContext {
public final class Content { public final class Content {
@ -45,10 +46,6 @@ public final class AnimatedAvatarSetContext {
} }
public func update(peers: [Peer], animated: Bool) -> Content { public func update(peers: [Peer], animated: Bool) -> Content {
for peer in peers {
}
var items: [(Content.Item.Key, Content.Item)] = [] var items: [(Content.Item.Key, Content.Item)] = []
for peer in peers { for peer in peers {
items.append((Content.Item.Key(peerId: peer.id), Content.Item(peer: peer))) items.append((Content.Item.Key(peerId: peer.id), Content.Item(peer: peer)))
@ -60,6 +57,7 @@ public final class AnimatedAvatarSetContext {
private let avatarFont = avatarPlaceholderFont(size: 12.0) private let avatarFont = avatarPlaceholderFont(size: 12.0)
private final class ContentNode: ASDisplayNode { private final class ContentNode: ASDisplayNode {
private var audioLevelView: VoiceBlobView?
private let unclippedNode: ASImageNode private let unclippedNode: ASImageNode
private let clippedNode: ASImageNode private let clippedNode: ASImageNode
@ -137,6 +135,51 @@ private final class ContentNode: ASDisplayNode {
self.clippedNode.alpha = isClipped ? 1.0 : 0.0 self.clippedNode.alpha = isClipped ? 1.0 : 0.0
} }
} }
func updateAudioLevel(color: UIColor, value: Float) {
if self.audioLevelView == nil, value > 0.0 {
let blobFrame = self.unclippedNode.bounds.insetBy(dx: -8.0, dy: -8.0)
let audioLevelView = VoiceBlobView(
frame: blobFrame,
maxLevel: 0.3,
smallBlobRange: (0, 0),
mediumBlobRange: (0.7, 0.8),
bigBlobRange: (0.8, 0.9)
)
let maskRect = CGRect(origin: .zero, size: blobFrame.size)
let playbackMaskLayer = CAShapeLayer()
playbackMaskLayer.frame = maskRect
playbackMaskLayer.fillRule = .evenOdd
let maskPath = UIBezierPath()
maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 12, dy: 12), cornerRadius: 22))
maskPath.append(UIBezierPath(rect: maskRect))
playbackMaskLayer.path = maskPath.cgPath
audioLevelView.layer.mask = playbackMaskLayer
audioLevelView.setColor(color)
self.audioLevelView = audioLevelView
self.view.insertSubview(audioLevelView, at: 0)
}
let level = min(1.0, max(0.0, CGFloat(value)))
if let audioLevelView = self.audioLevelView {
audioLevelView.updateLevel(CGFloat(value) * 2.0)
let avatarScale: CGFloat
if value > 0.0 {
audioLevelView.startAnimating()
avatarScale = 1.03 + level * 0.07
} else {
audioLevelView.stopAnimating(duration: 0.5)
avatarScale = 1.0
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
transition.updateSublayerTransformScale(node: self, scale: CGPoint(x: avatarScale, y: avatarScale), beginWithCurrentState: true)
}
}
} }
public final class AnimatedAvatarSetNode: ASDisplayNode { public final class AnimatedAvatarSetNode: ASDisplayNode {
@ -159,7 +202,9 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
var validKeys: [AnimatedAvatarSetContext.Content.Item.Key] = [] var validKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
var index = 0 var index = 0
for (key, item) in content.items { for i in 0 ..< content.items.count {
let (key, item) = content.items[i]
validKeys.append(key) validKeys.append(key)
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize) let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize)
@ -180,6 +225,7 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
} }
} }
itemNode.zPosition = CGFloat(100 - i)
contentWidth += itemSize.width - 10.0 contentWidth += itemSize.width - 10.0
index += 1 index += 1
} }
@ -201,4 +247,14 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
return CGSize(width: contentWidth, height: contentHeight) return CGSize(width: contentWidth, height: contentHeight)
} }
public func updateAudioLevels(color: UIColor, levels: [PeerId: Float]) {
for (key, itemNode) in self.contentNodes {
if let value = levels[key.peerId] {
itemNode.updateAudioLevel(color: color, value: value)
} else {
itemNode.updateAudioLevel(color: color, value: 0.0)
}
}
}
} }

View File

@ -608,7 +608,7 @@ open class NavigationController: UINavigationController, ContainableController,
} }
let effectiveModalTransition: CGFloat let effectiveModalTransition: CGFloat
if visibleModalCount == 0 { if visibleModalCount == 0 || navigationLayout.modal[i].isFlat {
effectiveModalTransition = 0.0 effectiveModalTransition = 0.0
} else if visibleModalCount == 1 { } else if visibleModalCount == 1 {
effectiveModalTransition = 1.0 - topModalDismissProgress effectiveModalTransition = 1.0 - topModalDismissProgress
@ -635,7 +635,7 @@ open class NavigationController: UINavigationController, ContainableController,
} }
if modalContainer.supernode != nil { if modalContainer.supernode != nil {
if !hasVisibleStandaloneModal && !isStandaloneModal { if !hasVisibleStandaloneModal && !isStandaloneModal && !modalContainer.isFlat {
visibleModalCount += 1 visibleModalCount += 1
} }
if isStandaloneModal { if isStandaloneModal {

View File

@ -109,9 +109,10 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
panRecognizer.delegate = self panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true panRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(panRecognizer) if !self.isFlat {
self.view.addGestureRecognizer(panRecognizer)
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
} }
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
@ -335,9 +336,13 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
switch layout.metrics.widthClass { switch layout.metrics.widthClass {
case .compact: case .compact:
self.panRecognizer?.isEnabled = true self.panRecognizer?.isEnabled = true
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
self.container.clipsToBounds = true self.container.clipsToBounds = true
if isStandaloneModal || isLandscape { if self.isFlat {
self.dim.backgroundColor = .clear
} else {
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
}
if isStandaloneModal || isLandscape || self.isFlat {
self.container.cornerRadius = 0.0 self.container.cornerRadius = 0.0
} else { } else {
self.container.cornerRadius = 10.0 self.container.cornerRadius = 10.0
@ -361,10 +366,9 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
containerFrame = unscaledFrame containerFrame = unscaledFrame
} else { } else {
topInset = 10.0 topInset = 10.0
if self.isFlat, let preferredSize = controllers.last?.preferredContentSizeForLayout(layout) { if self.isFlat {
topInset = layout.size.height - preferredSize.height topInset = 0.0
} } else if let statusBarHeight = layout.statusBarHeight {
if let statusBarHeight = layout.statusBarHeight {
topInset += statusBarHeight topInset += statusBarHeight
} }
@ -378,11 +382,20 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
} }
case .regular: case .regular:
self.panRecognizer?.isEnabled = false self.panRecognizer?.isEnabled = false
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.4) if self.isFlat {
self.container.clipsToBounds = true self.dim.backgroundColor = .clear
self.container.cornerRadius = 10.0 self.container.clipsToBounds = true
if #available(iOS 11.0, *) { self.container.cornerRadius = 0.0
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] if #available(iOS 11.0, *) {
self.container.layer.maskedCorners = []
}
} else {
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
self.container.clipsToBounds = true
self.container.cornerRadius = 10.0
if #available(iOS 11.0, *) {
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
} }
let verticalInset: CGFloat = 44.0 let verticalInset: CGFloat = 44.0
@ -408,6 +421,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
func animateIn(transition: ContainedViewLayoutTransition) { func animateIn(transition: ContainedViewLayoutTransition) {
if let controller = self.container.controllers.first, case .standaloneModal = controller.navigationPresentation { if let controller = self.container.controllers.first, case .standaloneModal = controller.navigationPresentation {
} else if self.isFlat {
} else { } else {
transition.updateAlpha(node: self.dim, alpha: 1.0) transition.updateAlpha(node: self.dim, alpha: 1.0)
transition.animatePositionAdditive(node: self.container, offset: CGPoint(x: 0.0, y: self.bounds.height + self.container.bounds.height / 2.0 - (self.container.position.y - self.bounds.height))) transition.animatePositionAdditive(node: self.container, offset: CGPoint(x: 0.0, y: self.bounds.height + self.container.bounds.height / 2.0 - (self.container.position.y - self.bounds.height)))
@ -429,7 +443,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
completion() completion()
return transition return transition
} else { } else {
if transition.isAnimated { if transition.isAnimated && !self.isFlat {
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true) alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true)

View File

@ -303,6 +303,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
private let emptyQueryDisposable = MetaDisposable() private let emptyQueryDisposable = MetaDisposable()
private let searchDisposable = MetaDisposable() private let searchDisposable = MetaDisposable()
private let forceTheme: PresentationTheme?
private var presentationData: PresentationData private var presentationData: PresentationData
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
@ -310,12 +311,16 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
private let presentationDataPromise: Promise<PresentationData> private let presentationDataPromise: Promise<PresentationData>
public init(context: AccountContext, peerId: PeerId, mode: ChannelMembersSearchMode, filters: [ChannelMembersSearchFilter], searchContext: GroupMembersSearchContext?, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, updateActivity: @escaping (Bool) -> Void, pushController: @escaping (ViewController) -> Void) { public init(context: AccountContext, forceTheme: PresentationTheme?, peerId: PeerId, mode: ChannelMembersSearchMode, filters: [ChannelMembersSearchFilter], searchContext: GroupMembersSearchContext?, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, updateActivity: @escaping (Bool) -> Void, pushController: @escaping (ViewController) -> Void) {
self.context = context self.context = context
self.openPeer = openPeer self.openPeer = openPeer
self.mode = mode self.mode = mode
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.forceTheme = forceTheme
if let forceTheme = self.forceTheme {
self.presentationData = self.presentationData.withUpdated(theme: forceTheme)
}
self.presentationDataPromise = Promise(self.presentationData) self.presentationDataPromise = Promise(self.presentationData)
self.emptyQueryListNode = ListView() self.emptyQueryListNode = ListView()
@ -622,9 +627,20 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError> let foundRemotePeers: Signal<([FoundPeer], [FoundPeer]), NoError>
switch mode { switch mode {
case .inviteActions, .banAndPromoteActions: case .inviteActions, .banAndPromoteActions:
foundContacts = context.account.postbox.searchContacts(query: query.lowercased()) if filters.contains(where: { filter in
foundRemotePeers = .single(([], [])) |> then(searchPeers(account: context.account, query: query) if case .excludeNonMembers = filter {
|> delay(0.2, queue: Queue.concurrentDefaultQueue())) return true
} else {
return false
}
}) {
foundContacts = .single(([], [:]))
foundRemotePeers = .single(([], []))
} else {
foundContacts = context.account.postbox.searchContacts(query: query.lowercased())
foundRemotePeers = .single(([], [])) |> then(searchPeers(account: context.account, query: query)
|> delay(0.2, queue: Queue.concurrentDefaultQueue()))
}
case .searchMembers, .searchBanned, .searchKicked, .searchAdmins: case .searchMembers, .searchBanned, .searchKicked, .searchAdmins:
foundContacts = .single(([], [:])) foundContacts = .single(([], [:]))
foundRemotePeers = .single(([], [])) foundRemotePeers = .single(([], []))
@ -639,7 +655,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
switch filter { switch filter {
case let .exclude(ids): case let .exclude(ids):
existingPeerIds = existingPeerIds.union(ids) existingPeerIds = existingPeerIds.union(ids)
case .disable: case .disable, .excludeNonMembers:
break break
} }
} }
@ -927,7 +943,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
switch filter { switch filter {
case let .exclude(ids): case let .exclude(ids):
existingPeerIds = existingPeerIds.union(ids) existingPeerIds = existingPeerIds.union(ids)
case .disable: case .disable, .excludeNonMembers:
break break
} }
} }
@ -1157,9 +1173,15 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
self.presentationDataDisposable = (context.sharedContext.presentationData self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in |> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self { if let strongSelf = self {
var presentationData = presentationData
let previousTheme = strongSelf.presentationData.theme let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings let previousStrings = strongSelf.presentationData.strings
if let forceTheme = strongSelf.forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
strongSelf.presentationData = presentationData strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {

View File

@ -9,17 +9,19 @@ import TelegramPresentationData
import AccountContext import AccountContext
import SearchUI import SearchUI
enum ChannelMembersSearchControllerMode { public enum ChannelMembersSearchControllerMode {
case promote case promote
case ban case ban
case inviteToCall
} }
public enum ChannelMembersSearchFilter { public enum ChannelMembersSearchFilter {
case exclude([PeerId]) case exclude([PeerId])
case disable([PeerId]) case disable([PeerId])
case excludeNonMembers
} }
final class ChannelMembersSearchController: ViewController { public final class ChannelMembersSearchController: ViewController {
private let queue = Queue() private let queue = Queue()
private let context: AccountContext private let context: AccountContext
@ -28,6 +30,7 @@ final class ChannelMembersSearchController: ViewController {
private let filters: [ChannelMembersSearchFilter] private let filters: [ChannelMembersSearchFilter]
private let openPeer: (Peer, RenderedChannelParticipant?) -> Void private let openPeer: (Peer, RenderedChannelParticipant?) -> Void
private let forceTheme: PresentationTheme?
private var presentationData: PresentationData private var presentationData: PresentationData
private var didPlayPresentationAnimation = false private var didPlayPresentationAnimation = false
@ -38,13 +41,17 @@ final class ChannelMembersSearchController: ViewController {
private var searchContentNode: NavigationBarSearchContentNode? private var searchContentNode: NavigationBarSearchContentNode?
init(context: AccountContext, peerId: PeerId, mode: ChannelMembersSearchControllerMode, filters: [ChannelMembersSearchFilter] = [], openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) { public init(context: AccountContext, peerId: PeerId, forceTheme: PresentationTheme? = nil, mode: ChannelMembersSearchControllerMode, filters: [ChannelMembersSearchFilter] = [], openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) {
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
self.mode = mode self.mode = mode
self.openPeer = openPeer self.openPeer = openPeer
self.filters = filters self.filters = filters
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.forceTheme = forceTheme
if let forceTheme = forceTheme {
self.presentationData = self.presentationData.withUpdated(theme: forceTheme)
}
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
@ -74,8 +81,8 @@ final class ChannelMembersSearchController: ViewController {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadDisplayNode() { override public func loadDisplayNode() {
self.displayNode = ChannelMembersSearchControllerNode(context: self.context, presentationData: self.presentationData, peerId: self.peerId, mode: self.mode, filters: self.filters) self.displayNode = ChannelMembersSearchControllerNode(context: self.context, presentationData: self.presentationData, forceTheme: self.forceTheme, peerId: self.peerId, mode: self.mode, filters: self.filters)
self.controllerNode.navigationBar = self.navigationBar self.controllerNode.navigationBar = self.navigationBar
self.controllerNode.requestActivateSearch = { [weak self] in self.controllerNode.requestActivateSearch = { [weak self] in
self?.activateSearch() self?.activateSearch()
@ -105,7 +112,7 @@ final class ChannelMembersSearchController: ViewController {
} }
} }
override func viewDidAppear(_ animated: Bool) { override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
@ -143,7 +150,7 @@ final class ChannelMembersSearchController: ViewController {
} }
} }
@objc func cancelPressed() { @objc private func cancelPressed() {
self.dismiss() self.dismiss()
} }
} }

View File

@ -112,18 +112,23 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
var requestOpenPeerFromSearch: ((Peer, RenderedChannelParticipant?) -> Void)? var requestOpenPeerFromSearch: ((Peer, RenderedChannelParticipant?) -> Void)?
var pushController: ((ViewController) -> Void)? var pushController: ((ViewController) -> Void)?
private let forceTheme: PresentationTheme?
var presentationData: PresentationData var presentationData: PresentationData
private var disposable: Disposable? private var disposable: Disposable?
private var listControl: PeerChannelMemberCategoryControl? private var listControl: PeerChannelMemberCategoryControl?
init(context: AccountContext, presentationData: PresentationData, peerId: PeerId, mode: ChannelMembersSearchControllerMode, filters: [ChannelMembersSearchFilter]) { init(context: AccountContext, presentationData: PresentationData, forceTheme: PresentationTheme?, peerId: PeerId, mode: ChannelMembersSearchControllerMode, filters: [ChannelMembersSearchFilter]) {
self.context = context self.context = context
self.listNode = ListView() self.listNode = ListView()
self.peerId = peerId self.peerId = peerId
self.mode = mode self.mode = mode
self.filters = filters self.filters = filters
self.presentationData = presentationData self.presentationData = presentationData
self.forceTheme = forceTheme
if let forceTheme = forceTheme {
self.presentationData = self.presentationData.withUpdated(theme: forceTheme)
}
super.init() super.init()
@ -190,7 +195,11 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
if ids.contains(peer.id) { if ids.contains(peer.id) {
continue continue
} }
case .disable: case let .disable(ids):
if ids.contains(peer.id) {
enabled = false
}
case .excludeNonMembers:
break break
} }
} }
@ -204,7 +213,11 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
if ids.contains(peer.id) { if ids.contains(peer.id) {
continue continue
} }
case .disable: case let .disable(ids):
if ids.contains(peer.id) {
enabled = false
}
case .excludeNonMembers:
break break
} }
} }
@ -212,6 +225,24 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
label = strongSelf.presentationData.strings.Channel_Management_LabelOwner label = strongSelf.presentationData.strings.Channel_Management_LabelOwner
enabled = false enabled = false
} }
case .inviteToCall:
if peer.id == context.account.peerId {
continue
}
for filter in filters {
switch filter {
case let .exclude(ids):
if ids.contains(peer.id) {
continue
}
case let .disable(ids):
if ids.contains(peer.id) {
enabled = false
}
case .excludeNonMembers:
break
}
}
} }
let renderedParticipant: RenderedChannelParticipant let renderedParticipant: RenderedChannelParticipant
switch participant { switch participant {
@ -262,7 +293,11 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
if ids.contains(participant.peer.id) { if ids.contains(participant.peer.id) {
continue continue
} }
case .disable: case let .disable(ids):
if ids.contains(participant.peer.id) {
enabled = false
}
case .excludeNonMembers:
break break
} }
} }
@ -276,7 +311,11 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
if ids.contains(participant.peer.id) { if ids.contains(participant.peer.id) {
continue continue
} }
case .disable: case let .disable(ids):
if ids.contains(participant.peer.id) {
enabled = false
}
case .excludeNonMembers:
break break
} }
} }
@ -284,6 +323,24 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
label = strongSelf.presentationData.strings.Channel_Management_LabelOwner label = strongSelf.presentationData.strings.Channel_Management_LabelOwner
enabled = false enabled = false
} }
case .inviteToCall:
if participant.peer.id == context.account.peerId {
continue
}
for filter in filters {
switch filter {
case let .exclude(ids):
if ids.contains(participant.peer.id) {
continue
}
case let .disable(ids):
if ids.contains(participant.peer.id) {
enabled = false
}
case .excludeNonMembers:
break
}
}
} }
entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled)) entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled))
index += 1 index += 1
@ -316,7 +373,10 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
func updatePresentationData(_ presentationData: PresentationData) { func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData self.presentationData = presentationData
self.searchDisplayController?.updatePresentationData(presentationData) if let forceTheme = forceTheme {
self.presentationData = self.presentationData.withUpdated(theme: forceTheme)
}
self.searchDisplayController?.updatePresentationData(self.presentationData)
} }
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
@ -350,7 +410,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
return return
} }
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ChannelMembersSearchContainerNode(context: self.context, peerId: self.peerId, mode: .banAndPromoteActions, filters: self.filters, searchContext: nil, openPeer: { [weak self] peer, participant in self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ChannelMembersSearchContainerNode(context: self.context, forceTheme: self.forceTheme, peerId: self.peerId, mode: .banAndPromoteActions, filters: self.filters, searchContext: nil, openPeer: { [weak self] peer, participant in
self?.requestOpenPeerFromSearch?(peer, participant) self?.requestOpenPeerFromSearch?(peer, participant)
}, updateActivity: { value in }, updateActivity: { value in

View File

@ -87,7 +87,7 @@ private final class ChannelMembersSearchItemNode: ItemListControllerSearchNode {
private let containerNode: ChannelMembersSearchContainerNode private let containerNode: ChannelMembersSearchContainerNode
init(context: AccountContext, peerId: PeerId, searchMode: ChannelMembersSearchMode, searchContext: GroupMembersSearchContext?, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, pushController: @escaping (ViewController) -> Void, dismissInput: @escaping () -> Void) { init(context: AccountContext, peerId: PeerId, searchMode: ChannelMembersSearchMode, searchContext: GroupMembersSearchContext?, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, pushController: @escaping (ViewController) -> Void, dismissInput: @escaping () -> Void) {
self.containerNode = ChannelMembersSearchContainerNode(context: context, peerId: peerId, mode: searchMode, filters: [], searchContext: searchContext, openPeer: { peer, participant in self.containerNode = ChannelMembersSearchContainerNode(context: context, forceTheme: nil, peerId: peerId, mode: searchMode, filters: [], searchContext: searchContext, openPeer: { peer, participant in
openPeer(peer, participant) openPeer(peer, participant)
}, updateActivity: updateActivity, pushController: pushController) }, updateActivity: updateActivity, pushController: pushController)
self.containerNode.cancel = { self.containerNode.cancel = {

View File

@ -15,7 +15,7 @@ import SearchBarNode
import SearchUI import SearchUI
import ChatListSearchItemHeader import ChatListSearchItemHeader
extension NavigationBarSearchContentNode: ItemListControllerSearchNavigationContentNode { /*extension NavigationBarSearchContentNode: ItemListControllerSearchNavigationContentNode {
public func activate() { public func activate() {
} }
@ -24,7 +24,7 @@ extension NavigationBarSearchContentNode: ItemListControllerSearchNavigationCont
public func setQueryUpdated(_ f: @escaping (String) -> Void) { public func setQueryUpdated(_ f: @escaping (String) -> Void) {
} }
} }*/
extension SettingsSearchableItemIcon { extension SettingsSearchableItemIcon {
func image() -> UIImage? { func image() -> UIImage? {

View File

@ -26,6 +26,155 @@ public enum LocationBroadcastPanelSource {
case peer(PeerId) case peer(PeerId)
} }
final class AccountGroupCallContextImpl: AccountGroupCallContext {
final class Proxy {
let context: AccountGroupCallContextImpl
let removed: () -> Void
init(context: AccountGroupCallContextImpl, removed: @escaping () -> Void) {
self.context = context
self.removed = removed
}
deinit {
self.removed()
}
func keep() {
}
}
var disposable: Disposable?
var participantsContext: GroupCallParticipantsContext?
private let panelDataPromise = Promise<GroupCallPanelData>()
var panelData: Signal<GroupCallPanelData, NoError> {
return self.panelDataPromise.get()
}
init(account: Account, peerId: PeerId, call: CachedChannelData.ActiveCall) {
self.panelDataPromise.set(.single(GroupCallPanelData(
peerId: peerId,
info: GroupCallInfo(
id: call.id,
accessHash: call.accessHash,
participantCount: 0,
clientParams: nil
),
topParticipants: [],
participantCount: 0,
activeSpeakers: Set(),
groupCall: nil
)))
self.disposable = (getGroupCallParticipants(account: account, callId: call.id, accessHash: call.accessHash, offset: "", limit: 100)
|> map(Optional.init)
|> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in
return .single(nil)
}
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self, let state = state else {
return
}
let context = GroupCallParticipantsContext(
account: account,
peerId: peerId,
id: call.id,
accessHash: call.accessHash,
state: state
)
strongSelf.participantsContext = context
strongSelf.panelDataPromise.set(combineLatest(queue: .mainQueue(),
context.state,
context.activeSpeakers
)
|> map { state, activeSpeakers -> GroupCallPanelData in
var topParticipants: [GroupCallParticipantsContext.Participant] = []
for participant in state.participants {
if topParticipants.count >= 3 {
break
}
topParticipants.append(participant)
}
return GroupCallPanelData(
peerId: peerId,
info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, clientParams: nil),
topParticipants: topParticipants,
participantCount: state.totalCount,
activeSpeakers: activeSpeakers,
groupCall: nil
)
})
})
}
deinit {
self.disposable?.dispose()
}
}
public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCache {
class Impl {
private class Record {
let context: AccountGroupCallContextImpl
let subscribers = Bag<Void>()
var removeTimer: SwiftSignalKit.Timer?
init(context: AccountGroupCallContextImpl) {
self.context = context
}
}
private let queue: Queue
private var contexts: [Int64: Record] = [:]
init(queue: Queue) {
self.queue = queue
}
func get(account: Account, peerId: PeerId, call: CachedChannelData.ActiveCall) -> AccountGroupCallContextImpl.Proxy {
let result: Record
if let current = self.contexts[call.id] {
result = current
} else {
let context = AccountGroupCallContextImpl(account: account, peerId: peerId, call: call)
result = Record(context: context)
self.contexts[call.id] = result
}
let index = result.subscribers.add(Void())
result.removeTimer?.invalidate()
result.removeTimer = nil
return AccountGroupCallContextImpl.Proxy(context: result.context, removed: { [weak self, weak result] in
Queue.mainQueue().async {
if let strongResult = result, let strongSelf = self, strongSelf.contexts[call.id] === strongResult {
strongResult.subscribers.remove(index)
if strongResult.subscribers.isEmpty {
let removeTimer = SwiftSignalKit.Timer(timeout: 30, repeat: false, completion: {
if let result = result, let strongSelf = self, strongSelf.contexts[call.id] === result, result.subscribers.isEmpty {
strongSelf.contexts.removeValue(forKey: call.id)
}
}, queue: .mainQueue())
strongResult.removeTimer = removeTimer
removeTimer.start()
}
}
}
})
}
}
let queue: Queue = .mainQueue()
let impl: QueueLocalObject<Impl>
public init() {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue)
})
}
}
private func presentLiveLocationController(context: AccountContext, peerId: PeerId, controller: ViewController) { private func presentLiveLocationController(context: AccountContext, peerId: PeerId, controller: ViewController) {
let presentImpl: (Message?) -> Void = { [weak controller] message in let presentImpl: (Message?) -> Void = { [weak controller] message in
if let message = message, let strongController = controller { if let message = message, let strongController = controller {
@ -265,7 +414,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
case .none, .all: case .none, .all:
break break
case let .peer(peerId): case let .peer(peerId):
let currentGroupCall: Signal<GroupCallPanelData?, NoError> = callManager.currentGroupCallSignal /*let currentGroupCall: Signal<GroupCallPanelData?, NoError> = callManager.currentGroupCallSignal
|> distinctUntilChanged(isEqual: { lhs, rhs in |> distinctUntilChanged(isEqual: { lhs, rhs in
return lhs?.internalId == rhs?.internalId return lhs?.internalId == rhs?.internalId
}) })
@ -295,7 +444,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
return SignalTakeAction(passthrough: true, complete: false) return SignalTakeAction(passthrough: true, complete: false)
} }
}) })
} }*/
let availableGroupCall: Signal<GroupCallPanelData?, NoError> let availableGroupCall: Signal<GroupCallPanelData?, NoError>
if case let .peer(peerId) = groupCallPanelSource { if case let .peer(peerId) = groupCallPanelSource {
@ -311,7 +460,30 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
guard let activeCall = activeCall else { guard let activeCall = activeCall else {
return .single(nil) return .single(nil)
} }
return getGroupCallParticipants(account: context.account, callId: activeCall.id, accessHash: activeCall.accessHash, offset: "", limit: 10)
return Signal { [weak context] subscriber in
guard let context = context, let callContextCache = context.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl else {
return EmptyDisposable
}
let disposable = MetaDisposable()
callContextCache.impl.syncWith { impl in
let callContext = impl.get(account: context.account, peerId: peerId, call: activeCall)
disposable.set((callContext.context.panelData
|> deliverOnMainQueue).start(next: { panelData in
callContext.keep()
subscriber.putNext(panelData)
}))
}
return ActionDisposable {
disposable.dispose()
}
}
|> runOn(.mainQueue())
/*let updatedData: Signal<GroupCallPanelData?, NoError> = getGroupCallParticipants(account: context.account, callId: activeCall.id, accessHash: activeCall.accessHash, offset: "", limit: 10)
|> map(Optional.init) |> map(Optional.init)
|> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in |> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in
return .single(nil) return .single(nil)
@ -365,20 +537,35 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
} }
|> runOn(.mainQueue()) |> runOn(.mainQueue())
} }
let initialData: Signal<GroupCallPanelData?, NoError> = .single(GroupCallPanelData(
peerId: peerId,
info: GroupCallInfo(
id: activeCall.id,
accessHash: activeCall.accessHash,
participantCount: 0,
clientParams: nil
),
topParticipants: [],
participantCount: 0,
numberOfActiveSpeakers: 0,
groupCall: nil
))
return initialData
|> then(updatedData)*/
} }
} else { } else {
availableGroupCall = .single(nil) availableGroupCall = .single(nil)
} }
self.currentGroupCallDisposable = combineLatest(queue: .mainQueue(), self.currentGroupCallDisposable = (availableGroupCall
currentGroupCall, |> deliverOnMainQueue).start(next: { [weak self] availableState in
availableGroupCall
).start(next: { [weak self] currentState, availableState in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
let panelData = currentState != nil ? nil : availableState let panelData = availableState
let wasEmpty = strongSelf.groupCallPanelData == nil let wasEmpty = strongSelf.groupCallPanelData == nil
strongSelf.groupCallPanelData = panelData strongSelf.groupCallPanelData = panelData

View File

@ -36,6 +36,7 @@ swift_library(
"//submodules/AudioBlob:AudioBlob", "//submodules/AudioBlob:AudioBlob",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/AlertUI:AlertUI", "//submodules/AlertUI:AlertUI",
"//submodules/PeerInfoUI:PeerInfoUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -27,7 +27,7 @@ public final class GroupCallPanelData {
public let info: GroupCallInfo public let info: GroupCallInfo
public let topParticipants: [GroupCallParticipantsContext.Participant] public let topParticipants: [GroupCallParticipantsContext.Participant]
public let participantCount: Int public let participantCount: Int
public let numberOfActiveSpeakers: Int public let activeSpeakers: Set<PeerId>
public let groupCall: PresentationGroupCall? public let groupCall: PresentationGroupCall?
public init( public init(
@ -35,18 +35,44 @@ public final class GroupCallPanelData {
info: GroupCallInfo, info: GroupCallInfo,
topParticipants: [GroupCallParticipantsContext.Participant], topParticipants: [GroupCallParticipantsContext.Participant],
participantCount: Int, participantCount: Int,
numberOfActiveSpeakers: Int, activeSpeakers: Set<PeerId>,
groupCall: PresentationGroupCall? groupCall: PresentationGroupCall?
) { ) {
self.peerId = peerId self.peerId = peerId
self.info = info self.info = info
self.topParticipants = topParticipants self.topParticipants = topParticipants
self.participantCount = participantCount self.participantCount = participantCount
self.numberOfActiveSpeakers = numberOfActiveSpeakers self.activeSpeakers = activeSpeakers
self.groupCall = groupCall self.groupCall = groupCall
} }
} }
private final class FakeAudioLevelGenerator {
private var previousTarget: Float = 0.0
private var nextTarget: Float = 0.0
private var nextTargetProgress: Float = 1.0
private var nextTargetProgressNorm: Float = 1.0
func get() -> Float {
self.nextTargetProgress *= 0.82
if self.nextTargetProgress <= 0.01 {
self.previousTarget = self.nextTarget
if Int.random(in: 0 ... 4) <= 1 {
self.nextTarget = 0.0
self.nextTargetProgressNorm = Float.random(in: 0.1 ..< 0.3)
} else {
self.nextTarget = Float.random(in: 0.0 ..< 20.0)
self.nextTargetProgressNorm = Float.random(in: 0.2 ..< 0.7)
}
self.nextTargetProgress = self.nextTargetProgressNorm
return self.nextTarget
} else {
let value = self.nextTarget * max(0.0, self.nextTargetProgress / self.nextTargetProgressNorm)
return value
}
}
}
public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
private let context: AccountContext private let context: AccountContext
private var theme: PresentationTheme private var theme: PresentationTheme
@ -77,6 +103,8 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
private let avatarsContext: AnimatedAvatarSetContext private let avatarsContext: AnimatedAvatarSetContext
private var avatarsContent: AnimatedAvatarSetContext.Content? private var avatarsContent: AnimatedAvatarSetContext.Content?
private let avatarsNode: AnimatedAvatarSetNode private let avatarsNode: AnimatedAvatarSetNode
private var audioLevelGenerators: [PeerId: FakeAudioLevelGenerator] = [:]
private var audioLevelGeneratorTimer: SwiftSignalKit.Timer?
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
@ -167,6 +195,7 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
deinit { deinit {
self.membersDisposable.dispose() self.membersDisposable.dispose()
self.isMutedDisposable.dispose() self.isMutedDisposable.dispose()
self.audioLevelGeneratorTimer?.invalidate()
} }
public override func didLoad() { public override func didLoad() {
@ -255,6 +284,8 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
let previousData = self.currentData let previousData = self.currentData
self.currentData = data self.currentData = data
var updateAudioLevels = false
if previousData?.groupCall !== data.groupCall { if previousData?.groupCall !== data.groupCall {
let membersText: String let membersText: String
if data.participantCount == 0 { if data.participantCount == 0 {
@ -382,11 +413,50 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
self.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: membersTextIsActive ? self.theme.chat.inputPanel.panelControlAccentColor : self.theme.chat.inputPanel.secondaryTextColor) self.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: membersTextIsActive ? self.theme.chat.inputPanel.panelControlAccentColor : self.theme.chat.inputPanel.secondaryTextColor)
self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false) self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false)
updateAudioLevels = true
} }
if let (size, leftInset, rightInset) = self.validLayout { if let (size, leftInset, rightInset) = self.validLayout {
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
} }
if updateAudioLevels {
for peerId in data.activeSpeakers {
if self.audioLevelGenerators[peerId] == nil {
self.audioLevelGenerators[peerId] = FakeAudioLevelGenerator()
}
}
var removeGenerators: [PeerId] = []
for peerId in self.audioLevelGenerators.keys {
if !data.activeSpeakers.contains(peerId) {
removeGenerators.append(peerId)
}
}
for peerId in removeGenerators {
self.audioLevelGenerators.removeValue(forKey: peerId)
}
if self.audioLevelGenerators.isEmpty {
self.audioLevelGeneratorTimer?.invalidate()
self.audioLevelGeneratorTimer = nil
self.avatarsNode.updateAudioLevels(color: self.theme.chat.inputPanel.actionControlFillColor, levels: [:])
} else if self.audioLevelGeneratorTimer == nil {
let audioLevelGeneratorTimer = SwiftSignalKit.Timer(timeout: 1.0 / 30.0, repeat: true, completion: { [weak self] in
self?.sampleAudioGenerators()
}, queue: .mainQueue())
self.audioLevelGeneratorTimer = audioLevelGeneratorTimer
audioLevelGeneratorTimer.start()
}
}
}
private func sampleAudioGenerators() {
var levels: [PeerId: Float] = [:]
for (peerId, generator) in self.audioLevelGenerators {
levels[peerId] = generator.get()
}
self.avatarsNode.updateAudioLevels(color: self.theme.chat.inputPanel.actionControlFillColor, levels: levels)
} }
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {

View File

@ -58,16 +58,16 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private struct SummaryParticipantsState: Equatable { private struct SummaryParticipantsState: Equatable {
public var participantCount: Int public var participantCount: Int
public var topParticipants: [GroupCallParticipantsContext.Participant] public var topParticipants: [GroupCallParticipantsContext.Participant]
public var numberOfActiveSpeakers: Int public var activeSpeakers: Set<PeerId>
public init( public init(
participantCount: Int, participantCount: Int,
topParticipants: [GroupCallParticipantsContext.Participant], topParticipants: [GroupCallParticipantsContext.Participant],
numberOfActiveSpeakers: Int activeSpeakers: Set<PeerId>
) { ) {
self.participantCount = participantCount self.participantCount = participantCount
self.topParticipants = topParticipants self.topParticipants = topParticipants
self.numberOfActiveSpeakers = numberOfActiveSpeakers self.activeSpeakers = activeSpeakers
} }
} }
@ -251,15 +251,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
return self.membersPromise.get() return self.membersPromise.get()
} }
private var invitedPeersValue: Set<PeerId> = Set() { private var invitedPeersValue: [PeerId] = [] {
didSet { didSet {
if self.invitedPeersValue != oldValue { if self.invitedPeersValue != oldValue {
self.inivitedPeersPromise.set(self.invitedPeersValue) self.inivitedPeersPromise.set(self.invitedPeersValue)
} }
} }
} }
private let inivitedPeersPromise = ValuePromise<Set<PeerId>>(Set()) private let inivitedPeersPromise = ValuePromise<[PeerId]>([])
public var invitedPeers: Signal<Set<PeerId>, NoError> { public var invitedPeers: Signal<[PeerId], NoError> {
return self.inivitedPeersPromise.get() return self.inivitedPeersPromise.get()
} }
@ -433,7 +433,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
participantCount: participantsState.participantCount, participantCount: participantsState.participantCount,
callState: callState, callState: callState,
topParticipants: participantsState.topParticipants, topParticipants: participantsState.topParticipants,
numberOfActiveSpeakers: participantsState.numberOfActiveSpeakers activeSpeakers: participantsState.activeSpeakers
) )
}) })
@ -520,6 +520,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [ strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_AnonymousDisabledAlertText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}) TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
]), on: .root, blockInteraction: false, completion: {}) ]), on: .root, blockInteraction: false, completion: {})
} else if case .tooManyParticipants = error {
let presentationData = strongSelf.accountContext.sharedContext.currentPresentationData.with { $0 }
strongSelf.accountContext.sharedContext.mainWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.VoiceChat_ChatFullAlertText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})
]), on: .root, blockInteraction: false, completion: {})
} }
strongSelf._canBeRemoved.set(.single(true)) strongSelf._canBeRemoved.set(.single(true))
})) }))
@ -615,9 +620,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.participantsContext = participantsContext self.participantsContext = participantsContext
self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(), self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(),
participantsContext.state, participantsContext.state,
participantsContext.numberOfActiveSpeakers |> deliverOnMainQueue, participantsContext.activeSpeakers |> deliverOnMainQueue,
self.speakingParticipantsContext.get() |> deliverOnMainQueue self.speakingParticipantsContext.get() |> deliverOnMainQueue
).start(next: { [weak self] state, numberOfActiveSpeakers, speakingParticipants in ).start(next: { [weak self] state, activeSpeakers, speakingParticipants in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
@ -685,7 +690,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState( strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState(
participantCount: state.totalCount, participantCount: state.totalCount,
topParticipants: topParticipants, topParticipants: topParticipants,
numberOfActiveSpeakers: numberOfActiveSpeakers activeSpeakers: activeSpeakers
))) )))
})) }))
@ -911,7 +916,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} }
var updatedInvitedPeers = self.invitedPeersValue var updatedInvitedPeers = self.invitedPeersValue
updatedInvitedPeers.insert(peerId) updatedInvitedPeers.insert(peerId, at: 0)
self.invitedPeersValue = updatedInvitedPeers self.invitedPeersValue = updatedInvitedPeers
let _ = inviteToGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start() let _ = inviteToGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start()

View File

@ -20,6 +20,7 @@ import DeleteChatPeerActionSheetItem
import UndoUI import UndoUI
import AlertUI import AlertUI
import PresentationDataUtils import PresentationDataUtils
import PeerInfoUI
private final class VoiceChatControllerTitleView: UIView { private final class VoiceChatControllerTitleView: UIView {
private var theme: PresentationTheme private var theme: PresentationTheme
@ -164,6 +165,7 @@ public final class VoiceChatController: ViewController {
enum State { enum State {
case listening case listening
case speaking case speaking
case invited
} }
var peer: Peer var peer: Peer
@ -288,7 +290,7 @@ public final class VoiceChatController: ViewController {
switch self { switch self {
case let .invite(_, _, text): case let .invite(_, _, text):
return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .generic(UIImage(bundleImageName: "Chat/Context Menu/AddUser")!), action: { return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .generic(UIImage(bundleImageName: "Chat/Context Menu/AddUser")!), action: {
interaction.openInvite()
}) })
case let .peer(peerEntry): case let .peer(peerEntry):
let peer = peerEntry.peer let peer = peerEntry.peer
@ -308,6 +310,9 @@ public final class VoiceChatController: ViewController {
case .speaking: case .speaking:
text = .text(presentationData.strings.VoiceChat_StatusSpeaking, .constructive) text = .text(presentationData.strings.VoiceChat_StatusSpeaking, .constructive)
icon = .microphone(false, UIColor(rgb: 0x34c759)) icon = .microphone(false, UIColor(rgb: 0x34c759))
case .invited:
text = .text(presentationData.strings.VoiceChat_StatusInvited, .generic)
icon = .invite(true)
} }
let revealOptions: [VoiceChatParticipantItem.RevealOption] = [] let revealOptions: [VoiceChatParticipantItem.RevealOption] = []
@ -362,6 +367,7 @@ public final class VoiceChatController: ViewController {
private var currentGroupMembers: [RenderedChannelParticipant]? private var currentGroupMembers: [RenderedChannelParticipant]?
private var currentCallMembers: [GroupCallParticipantsContext.Participant]? private var currentCallMembers: [GroupCallParticipantsContext.Participant]?
private var currentInvitedPeers: [Peer]?
private var currentSpeakingPeers: Set<PeerId>? private var currentSpeakingPeers: Set<PeerId>?
private var accountPeer: Peer? private var accountPeer: Peer?
@ -395,6 +401,8 @@ public final class VoiceChatController: ViewController {
private var itemInteraction: Interaction? private var itemInteraction: Interaction?
private let inviteDisposable = MetaDisposable()
init(controller: VoiceChatController, sharedContext: SharedAccountContext, call: PresentationGroupCall) { init(controller: VoiceChatController, sharedContext: SharedAccountContext, call: PresentationGroupCall) {
self.controller = controller self.controller = controller
self.sharedContext = sharedContext self.sharedContext = sharedContext
@ -442,11 +450,124 @@ public final class VoiceChatController: ViewController {
self?.call.updateMuteState(peerId: peerId, isMuted: isMuted) 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 navigationController = strongSelf.controller?.parentNavigationController {
strongSelf.controller?.dismiss() let context = strongSelf.context
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), keepStack: .always, purposefulAction: {}, peekData: nil)) strongSelf.controller?.dismiss(completion: {
Queue.mainQueue().justDispatch {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), keepStack: .always, purposefulAction: {}, peekData: nil))
}
})
}
}, openInvite: { [weak self] in
guard let strongSelf = self else {
return
} }
}, openInvite: {
let groupPeerId = strongSelf.call.peerId
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(groupPeerId)
}
|> deliverOnMainQueue).start(next: { groupPeer in
guard let strongSelf = self else {
return
}
guard let groupPeer = groupPeer as? TelegramChannel else {
return
}
var filters: [ChannelMembersSearchFilter] = []
if let currentCallMembers = strongSelf.currentCallMembers {
filters.append(.disable(Array(currentCallMembers.map { $0.peer.id })))
}
if !groupPeer.hasPermission(.inviteMembers) {
filters.append(.excludeNonMembers)
}
var dismissController: (() -> Void)?
let controller = ChannelMembersSearchController(context: strongSelf.context, peerId: groupPeer.id, forceTheme: strongSelf.darkTheme, mode: .inviteToCall, filters: filters, openPeer: { peer, participant in
guard let strongSelf = self else {
dismissController?()
return
}
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
if peer.id == strongSelf.context.account.peerId {
return
}
if let participant = participant {
strongSelf.call.invitePeer(participant.peer.id)
dismissController?()
} else {
let selfController = strongSelf.controller
let inviteDisposable = strongSelf.inviteDisposable
var inviteSignal = strongSelf.context.peerChannelMemberCategoriesContextsManager.addMembers(account: strongSelf.context.account, peerId: groupPeer.id, memberIds: [peer.id])
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
selfController?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
inviteSignal = inviteSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
inviteDisposable.set(nil)
}
inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(next: { _ in
guard let strongSelf = self else {
dismissController?()
return
}
strongSelf.call.invitePeer(peer.id)
dismissController?()
}, error: { error in
dismissController?()
guard let strongSelf = self else {
return
}
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let text: String
switch error {
case .limitExceeded:
text = presentationData.strings.Channel_ErrorAddTooMuch
case .tooMuchJoined:
text = presentationData.strings.Invite_ChannelsTooMuch
case .generic:
text = presentationData.strings.Login_UnknownError
case .restricted:
text = presentationData.strings.Channel_ErrorAddBlocked
case .notMutualContact:
text = presentationData.strings.GroupInfo_AddUserLeftError
case .botDoesntSupportGroups:
text = presentationData.strings.Channel_BotDoesntSupportGroups
case .tooMuchBots:
text = presentationData.strings.Channel_TooMuchBots
case .bot:
text = presentationData.strings.Login_UnknownError
}
strongSelf.controller?.present(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}))
}
})
dismissController = { [weak controller] in
controller?.dismiss()
}
strongSelf.controller?.push(controller)
})
}, peerContextAction: { [weak self] entry, sourceNode, gesture in }, peerContextAction: { [weak self] entry, sourceNode, gesture in
guard let strongSelf = self, let controller = strongSelf.controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else { guard let strongSelf = self, let controller = strongSelf.controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else {
return return
@ -543,15 +664,26 @@ public final class VoiceChatController: ViewController {
self.addSubnode(self.contentContainer) self.addSubnode(self.contentContainer)
self.contentContainer.addSubnode(self.backgroundNode) self.contentContainer.addSubnode(self.backgroundNode)
self.memberStatesDisposable = (self.call.members let context = self.context
|> deliverOnMainQueue).start(next: { [weak self] callMembers in let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers
|> mapToSignal { ids -> Signal<[Peer], NoError> in
return context.account.postbox.transaction { transaction -> [Peer] in
return ids.compactMap(transaction.getPeer)
}
}
self.memberStatesDisposable = combineLatest(queue: .mainQueue(),
self.call.members,
invitedPeers
).start(next: { [weak self] callMembers, invitedPeers in
guard let strongSelf = self, let callMembers = callMembers else { guard let strongSelf = self, let callMembers = callMembers else {
return return
} }
if let groupMembers = strongSelf.currentGroupMembers { if let groupMembers = strongSelf.currentGroupMembers {
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: groupMembers, callMembers: callMembers.participants, speakingPeers: callMembers.speakingParticipants) strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: groupMembers, callMembers: callMembers.participants, invitedPeers: invitedPeers, speakingPeers: callMembers.speakingParticipants)
} else { } else {
strongSelf.currentCallMembers = callMembers.participants strongSelf.currentCallMembers = callMembers.participants
strongSelf.currentInvitedPeers = invitedPeers
} }
let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers.totalCount))) let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers.totalCount)))
@ -574,7 +706,7 @@ public final class VoiceChatController: ViewController {
if !strongSelf.didSetDataReady { if !strongSelf.didSetDataReady {
strongSelf.accountPeer = accountPeer strongSelf.accountPeer = accountPeer
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: [], callMembers: strongSelf.currentCallMembers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set()) strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: [], callMembers: strongSelf.currentCallMembers ?? [], invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set())
if let peer = peerViewMainPeer(view), let channel = peer as? TelegramChannel { if let peer = peerViewMainPeer(view), let channel = peer as? TelegramChannel {
let addressName = channel.addressName ?? "" let addressName = channel.addressName ?? ""
@ -612,7 +744,7 @@ public final class VoiceChatController: ViewController {
} }
if wasMuted != (state.muteState != nil), let groupMembers = strongSelf.currentGroupMembers { if wasMuted != (state.muteState != nil), let groupMembers = strongSelf.currentGroupMembers {
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: groupMembers, callMembers: strongSelf.currentCallMembers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set()) strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: groupMembers, callMembers: strongSelf.currentCallMembers ?? [], invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set())
} }
if let (layout, navigationHeight) = strongSelf.validLayout { if let (layout, navigationHeight) = strongSelf.validLayout {
@ -781,6 +913,7 @@ public final class VoiceChatController: ViewController {
self.memberStatesDisposable?.dispose() self.memberStatesDisposable?.dispose()
self.audioLevelsDisposable?.dispose() self.audioLevelsDisposable?.dispose()
self.myAudioLevelDisposable?.dispose() self.myAudioLevelDisposable?.dispose()
self.inviteDisposable.dispose()
} }
override func didLoad() { override func didLoad() {
@ -860,7 +993,7 @@ public final class VoiceChatController: ViewController {
if let (layout, navigationHeight) = self.validLayout { if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
} }
self.updateMembers(muteState: self.effectiveMuteState, groupMembers: self.currentGroupMembers ?? [], callMembers: self.currentCallMembers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) self.updateMembers(muteState: self.effectiveMuteState, groupMembers: self.currentGroupMembers ?? [], callMembers: self.currentCallMembers ?? [], invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set())
case .ended, .cancelled: case .ended, .cancelled:
self.hapticFeedback.impact(.light) self.hapticFeedback.impact(.light)
@ -875,7 +1008,7 @@ public final class VoiceChatController: ViewController {
if let (layout, navigationHeight) = self.validLayout { if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
} }
self.updateMembers(muteState: self.effectiveMuteState, groupMembers: self.currentGroupMembers ?? [], callMembers: self.currentCallMembers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) self.updateMembers(muteState: self.effectiveMuteState, groupMembers: self.currentGroupMembers ?? [], callMembers: self.currentCallMembers ?? [], invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set())
default: default:
break break
} }
@ -1195,7 +1328,7 @@ public final class VoiceChatController: ViewController {
}) })
} }
private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, groupMembers: [RenderedChannelParticipant], callMembers: [GroupCallParticipantsContext.Participant], speakingPeers: Set<PeerId>) { private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, groupMembers: [RenderedChannelParticipant], callMembers: [GroupCallParticipantsContext.Participant], invitedPeers: [Peer], speakingPeers: Set<PeerId>) {
var sortedCallMembers = callMembers var sortedCallMembers = callMembers
sortedCallMembers.sort() sortedCallMembers.sort()
@ -1213,6 +1346,7 @@ public final class VoiceChatController: ViewController {
self.currentGroupMembers = groupMembers self.currentGroupMembers = groupMembers
self.currentCallMembers = callMembers self.currentCallMembers = callMembers
self.currentSpeakingPeers = speakingPeers self.currentSpeakingPeers = speakingPeers
self.currentInvitedPeers = invitedPeers
let previousEntries = self.currentEntries let previousEntries = self.currentEntries
var entries: [ListEntry] = [] var entries: [ListEntry] = []
@ -1221,7 +1355,7 @@ public final class VoiceChatController: ViewController {
var processedPeerIds = Set<PeerId>() var processedPeerIds = Set<PeerId>()
entries.append(.invite(self.presentationData.theme, self.presentationData.strings, "Invite Member")) entries.append(.invite(self.presentationData.theme, self.presentationData.strings, self.presentationData.strings.VoiceChat_InviteMember))
for member in callMembers { for member in callMembers {
if processedPeerIds.contains(member.peer.id) { if processedPeerIds.contains(member.peer.id) {
@ -1265,6 +1399,23 @@ public final class VoiceChatController: ViewController {
))) )))
} }
for peer in invitedPeers {
if processedPeerIds.contains(peer.id) {
continue
}
processedPeerIds.insert(peer.id)
entries.append(.peer(PeerEntry(
peer: peer,
presence: nil,
activityTimestamp: Int32.max - 1 - index,
state: .invited,
muteState: nil,
canManageCall: false
)))
index += 1
}
self.currentEntries = entries self.currentEntries = entries
let presentationData = self.presentationData.withUpdated(theme: self.darkTheme) let presentationData = self.presentationData.withUpdated(theme: self.darkTheme)
@ -1394,7 +1545,7 @@ public final class VoiceChatController: ViewController {
self.didAppearOnce = false self.didAppearOnce = false
completion?() completion?()
self.presentingViewController?.dismiss(animated: false) self.dismiss(animated: false)
} }
} }
@ -1405,7 +1556,7 @@ public final class VoiceChatController: ViewController {
self.controllerNode.animateOut(completion: { [weak self] in self.controllerNode.animateOut(completion: { [weak self] in
completion?() completion?()
self?.presentingViewController?.dismiss(animated: false) self?.dismiss(animated: false)
}) })
} }
} }

View File

@ -256,6 +256,7 @@ public func getGroupCallParticipants(account: Account, callId: Int64, accessHash
public enum JoinGroupCallError { public enum JoinGroupCallError {
case generic case generic
case anonymousNotAllowed case anonymousNotAllowed
case tooManyParticipants
} }
public struct JoinGroupCallResult { public struct JoinGroupCallResult {
@ -272,6 +273,8 @@ public func joinGroupCall(account: Account, peerId: PeerId, callId: Int64, acces
|> mapError { error -> JoinGroupCallError in |> mapError { error -> JoinGroupCallError in
if error.errorDescription == "GROUP_CALL_ANONYMOUS_FORBIDDEN" { if error.errorDescription == "GROUP_CALL_ANONYMOUS_FORBIDDEN" {
return .anonymousNotAllowed return .anonymousNotAllowed
} else if error.errorDescription == "GROUPCALL_PARTICIPANTS_TOO_MUCH" {
return .tooManyParticipants
} }
return .generic return .generic
} }
@ -633,16 +636,16 @@ public final class GroupCallParticipantsContext {
} }
} }
private var numberOfActiveSpeakersValue: Int = 0 { private var activeSpeakersValue: Set<PeerId> = Set() {
didSet { didSet {
if self.numberOfActiveSpeakersValue != oldValue { if self.activeSpeakersValue != oldValue {
self.numberOfActiveSpeakersPromise.set(self.numberOfActiveSpeakersValue) self.activeSpeakersPromise.set(self.activeSpeakersValue)
} }
} }
} }
private let numberOfActiveSpeakersPromise = ValuePromise<Int>(0) private let activeSpeakersPromise = ValuePromise<Set<PeerId>>(Set())
public var numberOfActiveSpeakers: Signal<Int, NoError> { public var activeSpeakers: Signal<Set<PeerId>, NoError> {
return self.numberOfActiveSpeakersPromise.get() return self.activeSpeakersPromise.get()
} }
private var updateQueue: [Update.StateUpdate] = [] private var updateQueue: [Update.StateUpdate] = []
@ -684,7 +687,9 @@ public final class GroupCallParticipantsContext {
return return
} }
strongSelf.numberOfActiveSpeakersValue = activities.count strongSelf.activeSpeakersValue = Set(activities.map { item -> PeerId in
item.0
})
if !strongSelf.hasReceivedSpeackingParticipantsReport { if !strongSelf.hasReceivedSpeackingParticipantsReport {
var updatedParticipants = strongSelf.stateValue.state.participants var updatedParticipants = strongSelf.stateValue.state.participants

View File

@ -10,14 +10,11 @@ import TelegramPresentationData
import AccountContext import AccountContext
import LiveLocationManager import LiveLocationManager
import TemporaryCachedPeerDataManager import TemporaryCachedPeerDataManager
#if ENABLE_WALLET
import WalletCore
import WalletUI
#endif
import PhoneNumberFormat import PhoneNumberFormat
import TelegramUIPreferences import TelegramUIPreferences
import TelegramVoip import TelegramVoip
import TelegramCallsUI import TelegramCallsUI
import TelegramBaseController
private final class DeviceSpecificContactImportContext { private final class DeviceSpecificContactImportContext {
let disposable = MetaDisposable() let disposable = MetaDisposable()
@ -111,10 +108,6 @@ public final class AccountContextImpl: AccountContext {
} }
public let account: Account public let account: Account
#if ENABLE_WALLET
public let tonContext: StoredTonContext?
#endif
public let fetchManager: FetchManager public let fetchManager: FetchManager
private let prefetchManager: PrefetchManager? private let prefetchManager: PrefetchManager?
@ -159,38 +152,12 @@ public final class AccountContextImpl: AccountContext {
private var experimentalUISettingsDisposable: Disposable? private var experimentalUISettingsDisposable: Disposable?
#if ENABLE_WALLET public let cachedGroupCallContexts: AccountGroupCallContextCache
public var hasWallets: Signal<Bool, NoError> {
return WalletStorageInterfaceImpl(postbox: self.account.postbox).getWalletRecords()
|> map { records in
return !records.isEmpty
}
}
public var hasWalletAccess: Signal<Bool, NoError> {
return self.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> map { view -> Bool in
guard let appConfiguration = view.values[PreferencesKeys.appConfiguration] as? AppConfiguration else {
return false
}
let walletConfiguration = WalletConfiguration.with(appConfiguration: appConfiguration)
if walletConfiguration.config != nil && walletConfiguration.blockchainName != nil {
return true
} else {
return false
}
}
|> distinctUntilChanged
}
#endif
public init(sharedContext: SharedAccountContextImpl, account: Account, /*tonContext: StoredTonContext?, */limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, temp: Bool = false) public init(sharedContext: SharedAccountContextImpl, account: Account, /*tonContext: StoredTonContext?, */limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, temp: Bool = false)
{ {
self.sharedContextImpl = sharedContext self.sharedContextImpl = sharedContext
self.account = account self.account = account
#if ENABLE_WALLET
self.tonContext = tonContext
#endif
self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager) self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager)
@ -216,6 +183,8 @@ public final class AccountContextImpl: AccountContext {
self.peersNearbyManager = nil self.peersNearbyManager = nil
} }
self.cachedGroupCallContexts = AccountGroupCallContextCacheImpl()
let updatedLimitsConfiguration = account.postbox.preferencesView(keys: [PreferencesKeys.limitsConfiguration]) let updatedLimitsConfiguration = account.postbox.preferencesView(keys: [PreferencesKeys.limitsConfiguration])
|> map { preferences -> LimitsConfiguration in |> map { preferences -> LimitsConfiguration in
return preferences.values[PreferencesKeys.limitsConfiguration] as? LimitsConfiguration ?? LimitsConfiguration.defaultValue return preferences.values[PreferencesKeys.limitsConfiguration] as? LimitsConfiguration ?? LimitsConfiguration.defaultValue

View File

@ -402,11 +402,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch chatLocation { switch chatLocation {
case let .peer(peerId): case let .peer(peerId):
locationBroadcastPanelSource = .peer(peerId) locationBroadcastPanelSource = .peer(peerId)
groupCallPanelSource = .peer(peerId) switch subject {
case .message, .none:
groupCallPanelSource = .peer(peerId)
default:
groupCallPanelSource = .none
}
self.chatLocationInfoData = .peer(Promise()) self.chatLocationInfoData = .peer(Promise())
case let .replyThread(replyThreadMessage): case let .replyThread(replyThreadMessage):
locationBroadcastPanelSource = .none locationBroadcastPanelSource = .none
groupCallPanelSource = .all groupCallPanelSource = .none
let promise = Promise<Message?>() let promise = Promise<Message?>()
let key = PostboxViewKey.messages([replyThreadMessage.messageId]) let key = PostboxViewKey.messages([replyThreadMessage.messageId])
promise.set(context.account.postbox.combinedView(keys: [key]) promise.set(context.account.postbox.combinedView(keys: [key])

View File

@ -5011,7 +5011,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}) })
} }
} else if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .members = currentPaneKey { } else if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .members = currentPaneKey {
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, contentNode: ChannelMembersSearchContainerNode(context: self.context, peerId: self.peerId, mode: .searchMembers, filters: [], searchContext: self.groupMembersSearchContext, openPeer: { [weak self] peer, participant in self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, contentNode: ChannelMembersSearchContainerNode(context: self.context, forceTheme: nil, peerId: self.peerId, mode: .searchMembers, filters: [], searchContext: self.groupMembersSearchContext, openPeer: { [weak self] peer, participant in
self?.openPeer(peerId: peer.id, navigation: .info) self?.openPeer(peerId: peer.id, navigation: .info)
}, updateActivity: { _ in }, updateActivity: { _ in
}, pushController: { [weak self] c in }, pushController: { [weak self] c in

View File

@ -637,12 +637,14 @@ public final class SharedAccountContextImpl: SharedAccountContext {
strongSelf.groupCallController = nil strongSelf.groupCallController = nil
strongSelf.hasOngoingCall.set(false) strongSelf.hasOngoingCall.set(false)
if let call = call { if let call = call, let navigationController = mainWindow.viewController as? NavigationController {
mainWindow.hostView.containerView.endEditing(true) mainWindow.hostView.containerView.endEditing(true)
let groupCallController = VoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call) let groupCallController = VoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call)
groupCallController.parentNavigationController = mainWindow.viewController as? NavigationController groupCallController.navigationPresentation = .flatModal
groupCallController.parentNavigationController = navigationController
strongSelf.groupCallController = groupCallController strongSelf.groupCallController = groupCallController
strongSelf.mainWindow?.present(groupCallController, on: .calls) //strongSelf.mainWindow?.present(groupCallController, on: .calls)
navigationController.pushViewController(groupCallController)
strongSelf.hasOngoingCall.set(true) strongSelf.hasOngoingCall.set(true)
} else { } else {
strongSelf.hasOngoingCall.set(false) strongSelf.hasOngoingCall.set(false)
@ -712,7 +714,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
if groupCallController.isNodeLoaded { if groupCallController.isNodeLoaded {
mainWindow.hostView.containerView.endEditing(true) mainWindow.hostView.containerView.endEditing(true)
if groupCallController.view.superview == nil { if groupCallController.view.superview == nil {
mainWindow.present(groupCallController, on: .calls) //mainWindow.present(groupCallController, on: .calls)
(mainWindow.viewController as? NavigationController)?.pushViewController(groupCallController)
} }
} }
} }
@ -987,7 +990,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
} else if let groupCallController = self.groupCallController { } else if let groupCallController = self.groupCallController {
if groupCallController.isNodeLoaded && groupCallController.view.superview == nil { if groupCallController.isNodeLoaded && groupCallController.view.superview == nil {
mainWindow.hostView.containerView.endEditing(true) mainWindow.hostView.containerView.endEditing(true)
mainWindow.present(groupCallController, on: .calls) //mainWindow.present(groupCallController, on: .calls)
(mainWindow.viewController as? NavigationController)?.pushViewController(groupCallController)
} }
} }
} }