diff --git a/Random.txt b/Random.txt index 9a9364694b..68d76a3e64 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -gCh0ST/jBZ+NM8mvcBcsd12A5FMFT4q6fETcWd5elO0= +E65Wt9QZyVD8tvGhCJD3My6x57eDORYaiYh6HR7T3fI= diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index b861fe2348..791530e7a5 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -186,17 +186,20 @@ public struct PresentationGroupCallSummaryState: Equatable { public var participantCount: Int public var callState: PresentationGroupCallState public var topParticipants: [GroupCallParticipantsContext.Participant] + public var numberOfActiveSpeakers: Int public init( info: GroupCallInfo, participantCount: Int, callState: PresentationGroupCallState, - topParticipants: [GroupCallParticipantsContext.Participant] + topParticipants: [GroupCallParticipantsContext.Participant], + numberOfActiveSpeakers: Int ) { self.info = info self.participantCount = participantCount self.callState = callState self.topParticipants = topParticipants + self.numberOfActiveSpeakers = numberOfActiveSpeakers } } @@ -221,6 +224,25 @@ public enum PresentationGroupCallMuteAction: Equatable { case unmuted } +public struct PresentationGroupCallMembers: Equatable { + public var participants: [GroupCallParticipantsContext.Participant] + public var speakingParticipants: Set + public var totalCount: Int + public var loadMoreToken: String? + + public init( + participants: [GroupCallParticipantsContext.Participant], + speakingParticipants: Set, + totalCount: Int, + loadMoreToken: String? + ) { + self.participants = participants + self.speakingParticipants = speakingParticipants + self.totalCount = totalCount + self.loadMoreToken = loadMoreToken + } +} + public protocol PresentationGroupCall: class { var account: Account { get } var accountContext: AccountContext { get } @@ -232,7 +254,7 @@ public protocol PresentationGroupCall: class { var canBeRemoved: Signal { get } var state: Signal { get } var summaryState: Signal { get } - var members: Signal<[PeerId: PresentationGroupCallMemberState], NoError> { get } + var members: Signal { get } var audioLevels: Signal<[(PeerId, Float)], NoError> { get } var myAudioLevel: Signal { get } var isMuted: Signal { get } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 6dbdb25bec..026f64da00 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1343,18 +1343,27 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var online = false var animateOnline = false + var onlineIsVoiceChat = false let peerRevealOptions: [ItemListRevealOption] let peerLeftRevealOptions: [ItemListRevealOption] switch item.content { case let .peer(_, renderedPeer, _, _, presence, _ ,_ ,_, _, _, displayAsMessage, _): - if !displayAsMessage, let peer = renderedPeer.peer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != item.context.account.peerId { - var updatedPresence = TelegramUserPresence(status: presence.status, lastActivity: 0) - let relativeStatus = relativeUserPresenceStatus(updatedPresence, relativeTo: timestamp) - if case .online = relativeStatus { - online = true + if !displayAsMessage { + if let peer = renderedPeer.peer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != item.context.account.peerId { + var updatedPresence = TelegramUserPresence(status: presence.status, lastActivity: 0) + let relativeStatus = relativeUserPresenceStatus(updatedPresence, relativeTo: timestamp) + if case .online = relativeStatus { + online = true + } + animateOnline = true + } else if let channel = renderedPeer.peer as? TelegramChannel { + onlineIsVoiceChat = true + if channel.flags.contains(.hasVoiceChat) { + online = true + animateOnline = true + } } - animateOnline = true } let isPinned = item.index.pinningIndex != nil @@ -1385,7 +1394,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { peerLeftRevealOptions = [] } - let (onlineLayout, onlineApply) = onlineLayout(online) + let (onlineLayout, onlineApply) = onlineLayout(online, onlineIsVoiceChat) var animateContent = false if let currentItem = currentItem, currentItem.content.chatLocation == item.content.chatLocation { animateContent = true diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 69d4afd6c6..a727f30b32 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -993,7 +993,7 @@ public final class ChatListNode: ListView { var cachedResult: [PeerId: [(Peer, PeerInputActivity)]] = [:] previousPeerCache.with { dict -> Void in for (chatPeerId, activities) in activitiesByPeerId { - if chatPeerId.threadId != nil { + guard case .global = chatPeerId.category else { continue } var cachedChatResult: [(Peer, PeerInputActivity)] = [] @@ -1015,7 +1015,7 @@ public final class ChatListNode: ListView { var result: [PeerId: [(Peer, PeerInputActivity)]] = [:] var peerCache: [PeerId: Peer] = [:] for (chatPeerId, activities) in activitiesByPeerId { - if chatPeerId.threadId != nil { + guard case .global = chatPeerId.category else { continue } var chatResult: [(Peer, PeerInputActivity)] = [] diff --git a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift index 9c9c87aeba..982245c3b5 100644 --- a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift +++ b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift @@ -178,7 +178,7 @@ public final class HorizontalPeerItemNode: ListViewItemNode { badgeSize += max(currentBadgeBackgroundImage.size.width, badgeLayout.size.width + 10.0) + 5.0 } - let (onlineLayout, onlineApply) = onlineLayout(online) + let (onlineLayout, onlineApply) = onlineLayout(online, false) var animateContent = false if let currentItem = currentItem, currentItem.peer.id == item.peer.id { animateContent = true diff --git a/submodules/PeerOnlineMarkerNode/Sources/PeerOnlineMarkerNode.swift b/submodules/PeerOnlineMarkerNode/Sources/PeerOnlineMarkerNode.swift index 3581cb4050..1943185352 100644 --- a/submodules/PeerOnlineMarkerNode/Sources/PeerOnlineMarkerNode.swift +++ b/submodules/PeerOnlineMarkerNode/Sources/PeerOnlineMarkerNode.swift @@ -24,8 +24,8 @@ public final class PeerOnlineMarkerNode: ASDisplayNode { self.iconNode.image = image } - public func asyncLayout() -> (Bool) -> (CGSize, (Bool) -> Void) { - return { [weak self] online in + public func asyncLayout() -> (Bool, Bool) -> (CGSize, (Bool) -> Void) { + return { [weak self] online, isVoiceChat in return (CGSize(width: 14.0, height: 14.0), { animated in if let strongSelf = self { strongSelf.iconNode.frame = CGRect(x: 0.0, y: 0.0, width: 14.0, height: 14.0) diff --git a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift index 6f4c470742..b73a032eec 100644 --- a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift +++ b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift @@ -163,7 +163,7 @@ public final class SelectablePeerNode: ASDisplayNode { self.avatarNode.setPeer(context: context, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, synchronousLoad: synchronousLoad) let onlineLayout = self.onlineNode.asyncLayout() - let (onlineSize, onlineApply) = onlineLayout(online) + let (onlineSize, onlineApply) = onlineLayout(online, false) let _ = onlineApply(false) self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(theme, state: .panel)) diff --git a/submodules/SyncCore/Sources/TelegramChannel.swift b/submodules/SyncCore/Sources/TelegramChannel.swift index b252e3d24d..58fa47db67 100644 --- a/submodules/SyncCore/Sources/TelegramChannel.swift +++ b/submodules/SyncCore/Sources/TelegramChannel.swift @@ -141,6 +141,7 @@ public struct TelegramChannelFlags: OptionSet { public static let isCreator = TelegramChannelFlags(rawValue: 1 << 1) public static let isScam = TelegramChannelFlags(rawValue: 1 << 2) public static let hasGeo = TelegramChannelFlags(rawValue: 1 << 3) + public static let hasVoiceChat = TelegramChannelFlags(rawValue: 1 << 4) } public final class TelegramChannel: Peer { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 8378cf68a4..45bc0c561a 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -503,7 +503,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1495959709] = { return Api.MessageReplyHeader.parse_messageReplyHeader($0) } dict[411017418] = { return Api.SecureValue.parse_secureValue($0) } dict[-316748368] = { return Api.SecureValueHash.parse_secureValueHash($0) } - dict[-1738792825] = { return Api.phone.GroupCall.parse_groupCall($0) } + dict[1722485756] = { return Api.phone.GroupCall.parse_groupCall($0) } dict[-398136321] = { return Api.messages.SearchCounter.parse_searchCounter($0) } dict[-2128698738] = { return Api.auth.CheckedPhone.parse_checkedPhone($0) } dict[-1188055347] = { return Api.PageListItem.parse_pageListItemText($0) } diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index bacd306092..5790653a8d 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -1649,21 +1649,16 @@ public struct photos { public extension Api { public struct phone { public enum GroupCall: TypeConstructorDescription { - case groupCall(call: Api.GroupCall, sources: [Int32], participants: [Api.GroupCallParticipant], participantsNextOffset: String, users: [Api.User]) + case groupCall(call: Api.GroupCall, participants: [Api.GroupCallParticipant], participantsNextOffset: String, users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .groupCall(let call, let sources, let participants, let participantsNextOffset, let users): + case .groupCall(let call, let participants, let participantsNextOffset, let users): if boxed { - buffer.appendInt32(-1738792825) + buffer.appendInt32(1722485756) } call.serialize(buffer, true) buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sources.count)) - for item in sources { - serializeInt32(item, buffer: buffer, boxed: false) - } - buffer.appendInt32(481674261) buffer.appendInt32(Int32(participants.count)) for item in participants { item.serialize(buffer, true) @@ -1680,8 +1675,8 @@ public struct phone { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .groupCall(let call, let sources, let participants, let participantsNextOffset, let users): - return ("groupCall", [("call", call), ("sources", sources), ("participants", participants), ("participantsNextOffset", participantsNextOffset), ("users", users)]) + case .groupCall(let call, let participants, let participantsNextOffset, let users): + return ("groupCall", [("call", call), ("participants", participants), ("participantsNextOffset", participantsNextOffset), ("users", users)]) } } @@ -1690,27 +1685,22 @@ public struct phone { if let signature = reader.readInt32() { _1 = Api.parse(reader, signature: signature) as? Api.GroupCall } - var _2: [Int32]? + var _2: [Api.GroupCallParticipant]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipant.self) } - var _3: [Api.GroupCallParticipant]? + var _3: String? + _3 = parseString(reader) + var _4: [Api.User]? if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipant.self) - } - var _4: String? - _4 = parseString(reader) - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.phone.GroupCall.groupCall(call: _1!, sources: _2!, participants: _3!, participantsNextOffset: _4!, users: _5!) + if _c1 && _c2 && _c3 && _c4 { + return Api.phone.GroupCall.groupCall(call: _1!, participants: _2!, participantsNextOffset: _3!, users: _4!) } else { return nil @@ -7372,13 +7362,23 @@ public extension Api { }) } - public static func getGroupParticipants(call: Api.InputGroupCall, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + public static func getGroupParticipants(call: Api.InputGroupCall, ids: [Int32], sources: [Int32], offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1374089052) + buffer.appendInt32(-906898811) call.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(ids.count)) + for item in ids { + serializeInt32(item, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sources.count)) + for item in sources { + serializeInt32(item, buffer: buffer, boxed: false) + } serializeString(offset, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "phone.getGroupParticipants", parameters: [("call", call), ("offset", offset), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.phone.GroupParticipants? in + return (FunctionDescription(name: "phone.getGroupParticipants", parameters: [("call", call), ("ids", ids), ("sources", sources), ("offset", offset), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.phone.GroupParticipants? in let reader = BufferReader(buffer) var result: Api.phone.GroupParticipants? if let signature = reader.readInt32() { diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index a7ff5e7c1f..4c8a7e9397 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -285,6 +285,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { info: summary.info, topParticipants: summary.topParticipants, participantCount: summary.participantCount, + numberOfActiveSpeakers: summary.numberOfActiveSpeakers, groupCall: call ) } @@ -304,21 +305,59 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { guard let activeCall = activeCall else { return .single(nil) } - return getCurrentGroupCall(account: context.account, callId: activeCall.id, accessHash: activeCall.accessHash) - |> `catch` { _ -> Signal in + return getGroupCallParticipants(account: context.account, callId: activeCall.id, accessHash: activeCall.accessHash, offset: "", limit: 10) + |> map(Optional.init) + |> `catch` { _ -> Signal in return .single(nil) } - |> map { summary -> GroupCallPanelData? in - guard let summary = summary else { - return nil + |> mapToSignal { initialState -> Signal in + guard let initialState = initialState else { + return .single(nil) } - return GroupCallPanelData( - peerId: peerId, - info: summary.info, - topParticipants: summary.topParticipants, - participantCount: summary.info.participantCount, - groupCall: nil - ) + + return Signal { subscriber in + let participantsContext = QueueLocalObject(queue: .mainQueue(), generate: { + return GroupCallParticipantsContext( + account: context.account, + peerId: peerId, + id: activeCall.id, + accessHash: activeCall.accessHash, + state: initialState + ) + }) + + let disposable = MetaDisposable() + participantsContext.with { participantsContext in + disposable.set(combineLatest(queue: .mainQueue(), + participantsContext.state, + participantsContext.numberOfActiveSpeakers + ).start(next: { state, numberOfActiveSpeakers in + var topParticipants: [GroupCallParticipantsContext.Participant] = [] + for participant in state.participants { + if topParticipants.count >= 3 { + break + } + topParticipants.append(participant) + } + let data = GroupCallPanelData( + peerId: peerId, + info: GroupCallInfo(id: activeCall.id, accessHash: activeCall.accessHash, participantCount: state.totalCount, clientParams: nil), + topParticipants: topParticipants, + participantCount: state.totalCount, + numberOfActiveSpeakers: numberOfActiveSpeakers, + groupCall: nil + ) + subscriber.putNext(data) + })) + } + + return ActionDisposable { + disposable.dispose() + participantsContext.with { _ in + } + } + } + |> runOn(.mainQueue()) } } } else { diff --git a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift new file mode 100644 index 0000000000..36b9b35f81 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift @@ -0,0 +1,155 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import AccountContext + + +private class CallStatusBarBackgroundNodeDrawingState: NSObject { + let amplitude: CGFloat + + let speaking: Bool + let transitionArguments: (startTime: Double, duration: Double)? + + init(amplitude: CGFloat, speaking: Bool, transitionArguments: (Double, Double)?) { + self.amplitude = amplitude + self.speaking = speaking + self.transitionArguments = transitionArguments + } +} + +private class CallStatusBarBackgroundNode: ASDisplayNode { + var muted = true + + var audioLevel: Float = 0.0 + var presentationAudioLevel: Float = 0.0 + + private var animator: ConstantDisplayLinkAnimator? + + override init() { + super.init() + + self.isOpaque = false + + self.updateAnimations() + } + + func updateAnimations() { + let animator: ConstantDisplayLinkAnimator + if let current = self.animator { + animator = current + } else { + animator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateAnimations() + }) + animator.frameInterval = 2 + self.animator = animator + } + animator.isPaused = true + + self.setNeedsDisplay() + } + + override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return CallStatusBarBackgroundNodeDrawingState(amplitude: 1.0, speaking: false, transitionArguments: nil) + } + + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + let drawStart = CACurrentMediaTime() + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + guard let parameters = parameters as? CallStatusBarBackgroundNodeDrawingState else { + return + } + + var locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] = [UIColor(rgb: 0x007fff).cgColor, UIColor(rgb:0x00afff).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: bounds.width, y: 0.0), options: CGGradientDrawingOptions()) + } +} + +public class CallStatusBarNodeImpl: CallStatusBarNode { + public enum Content { + case call(PresentationCall) + case groupCall(PresentationGroupCall) + } + + private let microphoneNode: VoiceChatMicrophoneNode + private let backgroundNode: CallStatusBarBackgroundNode + private let titleNode: ImmediateTextNode + private let subtitleNode: ImmediateTextNode + + private let audioLevelDisposable = MetaDisposable() + + private var currentSize: CGSize? + private var currentContent: Content? + + public override init() { + self.backgroundNode = CallStatusBarBackgroundNode() + self.microphoneNode = VoiceChatMicrophoneNode() + self.titleNode = ImmediateTextNode() + self.subtitleNode = ImmediateTextNode() + + super.init() + + self.addSubnode(self.backgroundNode) +// self.addSubnode(self.microphoneNode) +// self.addSubnode(self.titleNode) +// self.addSubnode(self.subtitleNode) + } + + deinit { + self.audioLevelDisposable.dispose() + } + + public func update(content: Content) { + self.currentContent = content + self.update() + } + + public override func update(size: CGSize) { + self.currentSize = size + self.update() + } + + private func update() { + guard let size = self.currentSize, let content = self.currentContent else { + return + } + + self.titleNode.attributedText = NSAttributedString(string: "Voice Chat", font: Font.semibold(13.0), textColor: .white) + self.subtitleNode.attributedText = NSAttributedString(string: "2 members", font: Font.regular(13.0), textColor: .white) + + let animationSize: CGFloat = 25.0 + let iconSpacing: CGFloat = 0.0 + let spacing: CGFloat = 5.0 + let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: size.height)) + let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: 160.0, height: size.height)) + + let totalWidth = animationSize + iconSpacing + titleSize.width + spacing + subtitleSize.width + let horizontalOrigin: CGFloat = floor((size.width - totalWidth) / 2.0) + + let contentHeight: CGFloat = 24.0 + let verticalOrigin: CGFloat = size.height - contentHeight + + self.microphoneNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin + floor((contentHeight - animationSize) / 2.0)), size: CGSize(width: animationSize, height: animationSize)) + self.microphoneNode.update(state: VoiceChatMicrophoneNode.State(muted: true, color: UIColor.white), animated: true) + + self.titleNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin + animationSize + iconSpacing, y: verticalOrigin + floor((contentHeight - titleSize.height) / 2.0)), size: titleSize) + self.subtitleNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin + animationSize + iconSpacing + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - subtitleSize.height) / 2.0)), size: subtitleSize) + + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) + } +} diff --git a/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift index 0cdfc1326f..bd6e8a9680 100644 --- a/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift +++ b/submodules/TelegramCallsUI/Sources/GroupCallNavigationAccessoryPanel.swift @@ -26,6 +26,7 @@ public final class GroupCallPanelData { public let info: GroupCallInfo public let topParticipants: [GroupCallParticipantsContext.Participant] public let participantCount: Int + public let numberOfActiveSpeakers: Int public let groupCall: PresentationGroupCall? public init( @@ -33,12 +34,14 @@ public final class GroupCallPanelData { info: GroupCallInfo, topParticipants: [GroupCallParticipantsContext.Participant], participantCount: Int, + numberOfActiveSpeakers: Int, groupCall: PresentationGroupCall? ) { self.peerId = peerId self.info = info self.topParticipants = topParticipants self.participantCount = participantCount + self.numberOfActiveSpeakers = numberOfActiveSpeakers self.groupCall = groupCall } } @@ -248,15 +251,28 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { } let membersText: String - if summaryState.participantCount == 0 { - membersText = strongSelf.strings.PeopleNearby_NoMembers + let membersTextIsActive: Bool + if summaryState.numberOfActiveSpeakers != 0 { + //TODO:localize + if summaryState.numberOfActiveSpeakers == 1 { + membersText = "1 member speaking" + } else { + membersText = "\(summaryState.numberOfActiveSpeakers) members speaking" + } + membersTextIsActive = true } else { - membersText = strongSelf.strings.Conversation_StatusMembers(Int32(summaryState.participantCount)) + if summaryState.participantCount == 0 { + membersText = strongSelf.strings.PeopleNearby_NoMembers + } else { + membersText = strongSelf.strings.Conversation_StatusMembers(Int32(summaryState.participantCount)) + } + membersTextIsActive = false } + strongSelf.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: membersTextIsActive ? strongSelf.theme.chat.inputPanel.panelControlAccentColor : strongSelf.theme.chat.inputPanel.secondaryTextColor) + strongSelf.avatarsContent = strongSelf.avatarsContext.update(peers: summaryState.topParticipants.map { $0.peer }, animated: false) - strongSelf.textNode.attributedText = NSAttributedString(string: membersText, font: Font.regular(13.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor) if let (size, leftInset, rightInset) = strongSelf.validLayout { strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) } @@ -272,15 +288,27 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode { } } else if data.groupCall == nil { let membersText: String - if data.participantCount == 0 { - membersText = self.strings.PeopleNearby_NoMembers + let membersTextIsActive: Bool + if data.numberOfActiveSpeakers != 0 { + //TODO:localize + if data.numberOfActiveSpeakers == 1 { + membersText = "1 member speaking" + } else { + membersText = "\(data.numberOfActiveSpeakers) members speaking" + } + membersTextIsActive = true } else { - membersText = self.strings.Conversation_StatusMembers(Int32(data.participantCount)) + if data.participantCount == 0 { + membersText = self.strings.PeopleNearby_NoMembers + } else { + membersText = self.strings.Conversation_StatusMembers(Int32(data.participantCount)) + } + membersTextIsActive = false } - self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false) + 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: self.theme.chat.inputPanel.secondaryTextColor) + self.avatarsContent = self.avatarsContext.update(peers: data.topParticipants.map { $0.peer }, animated: false) } if let (size, leftInset, rightInset) = self.validLayout { diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 16f4595588..94cbaf6263 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -57,13 +57,16 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private struct SummaryParticipantsState: Equatable { public var participantCount: Int public var topParticipants: [GroupCallParticipantsContext.Participant] + public var numberOfActiveSpeakers: Int public init( participantCount: Int, - topParticipants: [GroupCallParticipantsContext.Participant] + topParticipants: [GroupCallParticipantsContext.Participant], + numberOfActiveSpeakers: Int ) { self.participantCount = participantCount self.topParticipants = topParticipants + self.numberOfActiveSpeakers = numberOfActiveSpeakers } } @@ -205,6 +208,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var audioSessionActiveDisposable: Disposable? private var isAudioSessionActive = false + private let typingDisposable = MetaDisposable() + private let _canBeRemoved = Promise(false) public var canBeRemoved: Signal { return self._canBeRemoved.get() @@ -222,15 +227,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return self.statePromise.get() } - private var membersValue: [PeerId: PresentationGroupCallMemberState] = [:] { + private var membersValue: PresentationGroupCallMembers? { didSet { if self.membersValue != oldValue { self.membersPromise.set(self.membersValue) } } } - private let membersPromise = ValuePromise<[PeerId: PresentationGroupCallMemberState]>([:]) - public var members: Signal<[PeerId: PresentationGroupCallMemberState], NoError> { + private let membersPromise = ValuePromise(nil) + public var members: Signal { return self.membersPromise.get() } @@ -256,7 +261,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private var checkCallDisposable: Disposable? private var isCurrentlyConnecting: Bool? - + + private var myAudioLevelTimer: SwiftSignalKit.Timer? + public weak var sourcePanel: ASDisplayNode? init( @@ -397,7 +404,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { info: infoState.info, participantCount: participantsState.participantCount, callState: callState, - topParticipants: participantsState.topParticipants + topParticipants: participantsState.topParticipants, + numberOfActiveSpeakers: participantsState.numberOfActiveSpeakers ) }) @@ -419,6 +427,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.audioLevelsDisposable.dispose() self.participantsContextStateDisposable.dispose() self.myAudioLevelDisposable.dispose() + + self.myAudioLevelTimer?.invalidate() + self.typingDisposable.dispose() } private func updateSessionState(internalState: InternalState, audioSessionControl: ManagedAudioSessionControl?) { @@ -526,7 +537,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { guard let strongSelf = self else { return } - strongSelf.myAudioLevelPipe.putNext(level) + + let mappedLevel = level * 1.5 + + strongSelf.myAudioLevelPipe.putNext(mappedLevel) + strongSelf.processMyAudioLevel(level: mappedLevel) })) } } @@ -550,32 +565,39 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { let participantsContext = GroupCallParticipantsContext( account: self.accountContext.account, + peerId: self.peerId, id: callInfo.id, accessHash: callInfo.accessHash, state: initialState ) self.participantsContext = participantsContext - self.participantsContextStateDisposable.set((combineLatest(participantsContext.state, self.speakingParticipantsContext.get()) - |> deliverOnMainQueue).start(next: { [weak self] state, speakingParticipants in + self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(), + participantsContext.state, + participantsContext.numberOfActiveSpeakers, + self.speakingParticipantsContext.get() + ).start(next: { [weak self] state, numberOfActiveSpeakers, speakingParticipants in guard let strongSelf = self else { return } - var memberStates: [PeerId: PresentationGroupCallMemberState] = [:] var topParticipants: [GroupCallParticipantsContext.Participant] = [] + + var members = PresentationGroupCallMembers( + participants: [], + speakingParticipants: speakingParticipants, + totalCount: 0, + loadMoreToken: nil + ) for participant in state.participants { + members.participants.append(participant) + + if topParticipants.count < 3 { topParticipants.append(participant) } strongSelf.ssrcMapping[participant.ssrc] = participant.peer.id - memberStates[participant.peer.id] = PresentationGroupCallMemberState( - ssrc: participant.ssrc, - muteState: participant.muteState, - speaking: speakingParticipants.contains(participant.peer.id) - ) - if participant.peer.id == strongSelf.accountContext.account.peerId { if let muteState = participant.muteState { strongSelf.stateValue.muteState = muteState @@ -586,13 +608,18 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } } - strongSelf.membersValue = memberStates + + members.totalCount = state.totalCount + members.loadMoreToken = state.nextParticipantsFetchOffset + + strongSelf.membersValue = members strongSelf.stateValue.adminIds = state.adminIds strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState( participantCount: state.totalCount, - topParticipants: topParticipants + topParticipants: topParticipants, + numberOfActiveSpeakers: numberOfActiveSpeakers ))) })) @@ -813,4 +840,54 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { let _ = inviteToGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start() } + + private var currentMyAudioLevel: Float = 0.0 + private var currentMyAudioLevelTimestamp: Double = 0.0 + private var isSendingTyping: Bool = false + + private func restartMyAudioLevelTimer() { + self.myAudioLevelTimer?.invalidate() + let myAudioLevelTimer = SwiftSignalKit.Timer(timeout: 0.1, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.myAudioLevelTimer = nil + + let timestamp = CACurrentMediaTime() + + var shouldBeSendingTyping = false + if strongSelf.currentMyAudioLevel > 0.01 && timestamp < strongSelf.currentMyAudioLevelTimestamp + 1.0 { + strongSelf.restartMyAudioLevelTimer() + shouldBeSendingTyping = true + } else { + if timestamp < strongSelf.currentMyAudioLevelTimestamp + 1.0 { + strongSelf.restartMyAudioLevelTimer() + shouldBeSendingTyping = true + } + } + if shouldBeSendingTyping != strongSelf.isSendingTyping { + strongSelf.isSendingTyping = shouldBeSendingTyping + if shouldBeSendingTyping { + strongSelf.typingDisposable.set(strongSelf.accountContext.account.acquireLocalInputActivity(peerId: PeerActivitySpace(peerId: strongSelf.peerId, category: .voiceChat), activity: .speakingInGroupCall)) + strongSelf.restartMyAudioLevelTimer() + } else { + strongSelf.typingDisposable.set(nil) + } + } + }, queue: .mainQueue()) + self.myAudioLevelTimer = myAudioLevelTimer + myAudioLevelTimer.start() + } + + private func processMyAudioLevel(level: Float) { + self.currentMyAudioLevel = level + + if level > 0.01 { + self.currentMyAudioLevelTimestamp = CACurrentMediaTime() + + if self.myAudioLevelTimer == nil { + self.restartMyAudioLevelTimer() + } + } + } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift index f9fd11e989..b4c0386ebd 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift @@ -523,6 +523,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode { context.interpolationQuality = .low var appearanceProgress: CGFloat = 1.0 + var glowScale: CGFloat = 0.75 if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState { appearanceProgress = transition.progress(time: parameters.timestamp) } @@ -535,6 +536,8 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode { gradientTransition = 1.0 - gradientTransition } } + glowScale += gradientTransition * 0.3 + gradientImage = gradientTransition.isZero ? blobsState.blueGradient : blobsState.greenGradient if gradientTransition > 0.0 && gradientTransition < 1.0 { gradientImage = generateImage(CGSize(width: 100.0, height: 100.0), contextGenerator: { size, context in @@ -551,11 +554,10 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode { } context.saveGState() - var maskBounds = bounds - if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState { - let progress = 1.0 - appearanceProgress - maskBounds = maskBounds.insetBy(dx: bounds.width / 3.0 * progress, dy: bounds.width / 3.0 * progress) - } + + let progress = 1.0 - (appearanceProgress * glowScale) + let maskBounds = bounds.insetBy(dx: bounds.width / 3.0 * progress, dy: bounds.width / 3.0 * progress) + context.clip(to: maskBounds, mask: radialMaskImage.cgImage!) if let gradient = gradientImage?.cgImage { @@ -569,7 +571,8 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode { if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState { for blob in blobsState.blobs { if let path = blob.currentShape, let uiPath = path.copy() as? UIBezierPath { - let toOrigin = CGAffineTransform(translationX: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) + let offset = (bounds.size.width - blob.size.width) / 2.0 + let toOrigin = CGAffineTransform(translationX: -bounds.size.width / 2.0 + offset, y: -bounds.size.height / 2.0 + offset) let fromOrigin = CGAffineTransform(translationX: bounds.size.width / 2.0, y: bounds.size.height / 2.0) uiPath.apply(toOrigin) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 8ee35dcaa2..f1cd9aba43 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -272,8 +272,9 @@ public final class VoiceChatController: ViewController { private var didSetContentsReady: Bool = false private var didSetDataReady: Bool = false - private var currentMembers: [RenderedChannelParticipant]? - private var currentMemberStates: [PeerId: PresentationGroupCallMemberState]? + private var currentGroupMembers: [RenderedChannelParticipant]? + private var currentCallMembers: [GroupCallParticipantsContext.Participant]? + private var currentSpeakingPeers: Set? private var currentInvitedPeers: Set? private var currentEntries: [PeerEntry] = [] @@ -470,19 +471,19 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - strongSelf.updateMembers(muteState: strongSelf.callState?.muteState, members: state.list, memberStates: strongSelf.currentMemberStates ?? [:], invitedPeers: strongSelf.currentInvitedPeers ?? Set()) + strongSelf.updateMembers(muteState: strongSelf.callState?.muteState, groupMembers: state.list, callMembers: strongSelf.currentCallMembers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set(), invitedPeers: strongSelf.currentInvitedPeers ?? Set()) } }) self.memberStatesDisposable = (self.call.members - |> deliverOnMainQueue).start(next: { [weak self] memberStates in - guard let strongSelf = self else { + |> deliverOnMainQueue).start(next: { [weak self] callMembers in + guard let strongSelf = self, let callMembers = callMembers else { return } - if let members = strongSelf.currentMembers { - strongSelf.updateMembers(muteState: strongSelf.callState?.muteState, members: members, memberStates: memberStates, invitedPeers: strongSelf.currentInvitedPeers ?? Set()) + if let groupMembers = strongSelf.currentGroupMembers { + strongSelf.updateMembers(muteState: strongSelf.callState?.muteState, groupMembers: groupMembers, callMembers: callMembers.participants, speakingPeers: callMembers.speakingParticipants ?? Set(), invitedPeers: strongSelf.currentInvitedPeers ?? Set()) } else { - strongSelf.currentMemberStates = memberStates + strongSelf.currentCallMembers = callMembers.participants } }) @@ -491,8 +492,8 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - if let members = strongSelf.currentMembers { - strongSelf.updateMembers(muteState: strongSelf.callState?.muteState, members: members, memberStates: strongSelf.currentMemberStates ?? [:], invitedPeers: invitedPeers) + if let groupMembers = strongSelf.currentGroupMembers { + strongSelf.updateMembers(muteState: strongSelf.callState?.muteState, groupMembers: groupMembers, callMembers: strongSelf.currentCallMembers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set(), invitedPeers: invitedPeers) } else { strongSelf.currentInvitedPeers = invitedPeers } @@ -554,8 +555,8 @@ public final class VoiceChatController: ViewController { } } - if wasMuted != (state.muteState != nil), let members = strongSelf.currentMembers { - strongSelf.updateMembers(muteState: state.muteState, members: members, memberStates: strongSelf.currentMemberStates ?? [:], invitedPeers: strongSelf.currentInvitedPeers ?? Set()) + if wasMuted != (state.muteState != nil), let groupMembers = strongSelf.currentGroupMembers { + strongSelf.updateMembers(muteState: state.muteState, groupMembers: groupMembers, callMembers: strongSelf.currentCallMembers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set(), invitedPeers: strongSelf.currentInvitedPeers ?? Set()) } if let (layout, navigationHeight) = strongSelf.validLayout { @@ -1094,28 +1095,43 @@ public final class VoiceChatController: ViewController { }) } - private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, members: [RenderedChannelParticipant], memberStates: [PeerId: PresentationGroupCallMemberState], invitedPeers: Set) { - var members = members - members.sort(by: { lhs, rhs in + private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, groupMembers: [RenderedChannelParticipant], callMembers: [GroupCallParticipantsContext.Participant], speakingPeers: Set, invitedPeers: Set) { + var groupMembers = groupMembers + groupMembers.sort(by: { lhs, rhs in if lhs.peer.id == self.context.account.peerId { return true } else if rhs.peer.id == self.context.account.peerId { return false } - let lhsHasState = memberStates[lhs.peer.id] != nil - let rhsHasState = memberStates[rhs.peer.id] != nil - if lhsHasState != rhsHasState { - if lhsHasState { - return true - } else { - return false - } + + let lhsPresence = lhs.presences[lhs.peer.id] + let rhsPresence = lhs.presences[lhs.peer.id] + + if let lhsPresence = lhsPresence as? TelegramUserPresence, let rhsPresence = rhsPresence as? TelegramUserPresence { + return lhsPresence.status > rhsPresence.status + } else if let _ = lhsPresence as? TelegramUserPresence { + return true + } else if let _ = rhsPresence as? TelegramUserPresence { + return false } + return lhs.peer.id < rhs.peer.id }) - self.currentMembers = members - self.currentMemberStates = memberStates + var callMembers = callMembers + + for i in 0 ..< callMembers.count { + if callMembers[i].peer.id == self.context.account.peerId { + let member = callMembers[i] + callMembers.remove(at: i) + callMembers.insert(member, at: i) + break + } + } + + self.currentGroupMembers = groupMembers + self.currentCallMembers = callMembers + self.currentSpeakingPeers = speakingPeers self.currentInvitedPeers = invitedPeers let previousEntries = self.currentEntries @@ -1123,7 +1139,44 @@ public final class VoiceChatController: ViewController { var index: Int32 = 0 - for member in members { + var processedPeerIds = Set() + + for member in callMembers { + if processedPeerIds.contains(member.peer.id) { + continue + } + processedPeerIds.insert(member.peer.id) + + let memberState: PeerEntry.State + var memberMuteState: GroupCallParticipantsContext.Participant.MuteState? + if member.peer.id == self.context.account.peerId { + if muteState == nil { + memberState = .speaking + } else { + memberState = .listening + } + } else { + memberState = speakingPeers.contains(member.peer.id) ? .speaking : .listening + memberMuteState = member.muteState + } + + entries.append(PeerEntry( + peer: member.peer, + presence: nil, + activityTimestamp: Int32.max - 1 - index, + state: memberState, + muteState: memberMuteState, + invited: false + )) + index += 1 + } + + for member in groupMembers { + if processedPeerIds.contains(member.peer.id) { + continue + } + processedPeerIds.insert(member.peer.id) + if let user = member.peer as? TelegramUser, user.botInfo != nil || user.isDeleted { continue } @@ -1136,9 +1189,6 @@ public final class VoiceChatController: ViewController { } else { memberState = .listening } - } else if let state = memberStates[member.peer.id] { - memberState = state.speaking ? .speaking : .listening - memberMuteState = state.muteState } else { memberState = .inactive } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index a05ac787e8..569c3ef2d8 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -624,6 +624,7 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { strongSelf.actionButtonNode.addSubnode(animationNode) } animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, color: color), animated: true) + strongSelf.actionButtonNode.isUserInteractionEnabled = false } else if let animationNode = strongSelf.animationNode { strongSelf.animationNode = nil animationNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak animationNode] _ in @@ -647,6 +648,7 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } else { iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: item.presentationData.theme.list.itemAccentColor) } + strongSelf.actionButtonNode.isUserInteractionEnabled = !invited } else if let iconNode = strongSelf.iconNode { strongSelf.iconNode = nil iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak iconNode] _ in diff --git a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift index 821824b844..6e8851554f 100644 --- a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift @@ -1221,21 +1221,42 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo updatedState.readSecretOutbox(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: chatId), timestamp: maxDate, actionTimestamp: date) case let .updateUserTyping(userId, type): if let date = updatesDate, date + 60 > serverTime { - updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), threadId: nil), peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), activity: PeerInputActivity(apiType: type)) + let activity = PeerInputActivity(apiType: type) + var category: PeerActivitySpace.Category = .global + if case .speakingInGroupCall = activity { + category = .voiceChat + } + + updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), category: category), peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), activity: activity) } case let .updateChatUserTyping(chatId, userId, type): if let date = updatesDate, date + 60 > serverTime { - updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId), threadId: nil), peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), activity: PeerInputActivity(apiType: type)) + let activity = PeerInputActivity(apiType: type) + var category: PeerActivitySpace.Category = .global + if case .speakingInGroupCall = activity { + category = .voiceChat + } + + updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: chatId), category: category), peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), activity: activity) } case let .updateChannelUserTyping(_, channelId, topMsgId, userId, type): if let date = updatesDate, date + 60 > serverTime { let channelPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) let threadId = topMsgId.flatMap { makeMessageThreadId(MessageId(peerId: channelPeerId, namespace: Namespaces.Message.Cloud, id: $0)) } - updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: channelPeerId, threadId: threadId), peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), activity: PeerInputActivity(apiType: type)) + + let activity = PeerInputActivity(apiType: type) + var category: PeerActivitySpace.Category = .global + if case .speakingInGroupCall = activity { + category = .voiceChat + } else if let threadId = threadId { + category = .thread(threadId) + } + + updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: channelPeerId, category: category), peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), activity: activity) } case let .updateEncryptedChatTyping(chatId): if let date = updatesDate, date + 60 > serverTime { - updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: chatId), threadId: nil), peerId: nil, activity: .typingText) + updatedState.addPeerInputActivity(chatPeerId: PeerActivitySpace(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: chatId), category: .global), peerId: nil, activity: .typingText) } case let .updateDialogPinned(flags, folderId, peer): let groupId: PeerGroupId = folderId.flatMap(PeerGroupId.init(rawValue:)) ?? .root @@ -2342,16 +2363,16 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP let chatPeerId = message.id.peerId if let authorId = message.authorId { let activityValue: PeerInputActivity? = nil - if updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: nil)] == nil { - updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: nil)] = [authorId: activityValue] + if updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .global)] == nil { + updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .global)] = [authorId: activityValue] } else { - updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: nil)]![authorId] = activityValue + updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .global)]![authorId] = activityValue } if let threadId = message.threadId { - if updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: threadId)] == nil { - updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: threadId)] = [authorId: activityValue] + if updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .thread(threadId))] == nil { + updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .thread(threadId))] = [authorId: activityValue] } else { - updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: threadId)]![authorId] = activityValue + updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .thread(threadId))]![authorId] = activityValue } } } @@ -3230,10 +3251,10 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP if let peer = transaction.getPeer(chatPeerId) as? TelegramSecretChat { let authorId = peer.regularPeerId let activityValue: PeerInputActivity? = .typingText - if updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: nil)] == nil { - updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: nil)] = [authorId: activityValue] + if updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .global)] == nil { + updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .global)] = [authorId: activityValue] } else { - updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: nil)]![authorId] = activityValue + updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .global)]![authorId] = activityValue } } } @@ -3278,10 +3299,10 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP for (chatPeerId, authorId) in addedSecretMessageAuthorIds { let activityValue: PeerInputActivity? = nil - if updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: nil)] == nil { - updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: nil)] = [authorId: activityValue] + if updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .global)] == nil { + updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .global)] = [authorId: activityValue] } else { - updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, threadId: nil)]![authorId] = activityValue + updatedTypingActivities[PeerActivitySpace(peerId: chatPeerId, category: .global)]![authorId] = activityValue } } diff --git a/submodules/TelegramCore/Sources/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiGroupOrChannel.swift index 5bbb5a10eb..4b91c29b12 100644 --- a/submodules/TelegramCore/Sources/ApiGroupOrChannel.swift +++ b/submodules/TelegramCore/Sources/ApiGroupOrChannel.swift @@ -97,6 +97,9 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { if (flags & Int32(1 << 21)) != 0 { channelFlags.insert(.hasGeo) } + if (flags & Int32(1 << 23)) != 0 { + channelFlags.insert(.hasVoiceChat) + } let restrictionInfo: PeerAccessRestrictionInfo? if let restrictionReason = restrictionReason { diff --git a/submodules/TelegramCore/Sources/GroupCalls.swift b/submodules/TelegramCore/Sources/GroupCalls.swift index c300f2ea72..3e6867c6c2 100644 --- a/submodules/TelegramCore/Sources/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/GroupCalls.swift @@ -9,6 +9,18 @@ public struct GroupCallInfo: Equatable { public var accessHash: Int64 public var participantCount: Int public var clientParams: String? + + public init( + id: Int64, + accessHash: Int64, + participantCount: Int, + clientParams: String? + ) { + self.id = id + self.accessHash = accessHash + self.participantCount = participantCount + self.clientParams = clientParams + } } public struct GroupCallSummary: Equatable { @@ -50,7 +62,7 @@ public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int } |> mapToSignal { result -> Signal in switch result { - case let .groupCall(call, _, participants, _, users): + case let .groupCall(call, participants, _, users): return account.postbox.transaction { transaction -> GroupCallSummary? in guard let info = GroupCallInfo(call) else { return nil @@ -165,7 +177,7 @@ public enum GetGroupCallParticipantsError { } public func getGroupCallParticipants(account: Account, callId: Int64, accessHash: Int64, offset: String, limit: Int32) -> Signal { - return account.network.request(Api.functions.phone.getGroupParticipants(call: .inputGroupCall(id: callId, accessHash: accessHash), offset: offset, limit: limit)) + return account.network.request(Api.functions.phone.getGroupParticipants(call: .inputGroupCall(id: callId, accessHash: accessHash), ids: [], sources: [], offset: offset, limit: limit)) |> mapError { _ -> GetGroupCallParticipantsError in return .generic } @@ -341,7 +353,7 @@ public func joinGroupCall(account: Account, peerId: PeerId, callId: Int64, acces state.adminIds = adminIds switch result { - case let .groupCall(call, sources, _, _, users): + case let .groupCall(call, _, _, users): guard let _ = GroupCallInfo(call) else { return .fail(.generic) } @@ -385,7 +397,11 @@ public func leaveGroupCall(account: Account, callId: Int64, accessHash: Int64, s |> mapError { _ -> LeaveGroupCallError in return .generic } - |> ignoreValues + |> mapToSignal { result -> Signal in + account.stateManager.addUpdates(result) + + return .complete() + } } public enum StopGroupCallError { @@ -569,20 +585,63 @@ public final class GroupCallParticipantsContext { } } + private var numberOfActiveSpeakersValue: Int = 0 { + didSet { + if self.numberOfActiveSpeakersValue != oldValue { + self.numberOfActiveSpeakersPromise.set(self.numberOfActiveSpeakersValue) + } + } + } + private let numberOfActiveSpeakersPromise = ValuePromise(0) + public var numberOfActiveSpeakers: Signal { + return self.numberOfActiveSpeakersPromise.get() + } + private var updateQueue: [StateUpdate] = [] private var isProcessingUpdate: Bool = false private let disposable = MetaDisposable() - public init(account: Account, id: Int64, accessHash: Int64, state: State) { + private let updatesDisposable = MetaDisposable() + private var activitiesDisposable: Disposable? + + public init(account: Account, peerId: PeerId, id: Int64, accessHash: Int64, state: State) { self.account = account self.id = id self.accessHash = accessHash self.stateValue = InternalState(state: state, overlayState: OverlayState()) self.statePromise = ValuePromise(self.stateValue) + + self.updatesDisposable.set((self.account.stateManager.groupCallParticipantUpdates + |> deliverOnMainQueue).start(next: { [weak self] updates in + guard let strongSelf = self else { + return + } + var filteredUpdates: [StateUpdate] = [] + for (callId, update) in updates { + if callId == id { + filteredUpdates.append(update) + } + } + if !filteredUpdates.isEmpty { + strongSelf.addUpdates(updates: filteredUpdates) + } + })) + + let activityCategory: PeerActivitySpace.Category = .voiceChat + self.activitiesDisposable = (self.account.peerInputActivities(peerId: PeerActivitySpace(peerId: peerId, category: activityCategory)) + |> deliverOnMainQueue).start(next: { [weak self] activities in + guard let strongSelf = self else { + return + } + + strongSelf.numberOfActiveSpeakersValue = activities.count + }) } deinit { self.disposable.dispose() + self.updatesDisposable.dispose() + self.activitiesDisposable?.dispose() } public func addUpdates(updates: [StateUpdate]) { @@ -625,6 +684,8 @@ public final class GroupCallParticipantsContext { return } + let isVersionUpdate = update.version != self.stateValue.state.version + let _ = (self.account.postbox.transaction { transaction -> [PeerId: Peer] in var peers: [PeerId: Peer] = [:] @@ -648,7 +709,9 @@ public final class GroupCallParticipantsContext { if participantUpdate.isRemoved { if let index = updatedParticipants.firstIndex(where: { $0.peer.id == participantUpdate.peerId }) { updatedParticipants.remove(at: index) - updatedTotalCount -= 1 + updatedTotalCount = max(0, updatedTotalCount - 1) + } else if isVersionUpdate { + updatedTotalCount = max(0, updatedTotalCount - 1) } } else { guard let peer = peers[participantUpdate.peerId] else { diff --git a/submodules/TelegramCore/Sources/ManagedLocalInputActivities.swift b/submodules/TelegramCore/Sources/ManagedLocalInputActivities.swift index 661d36379c..c8a02783c8 100644 --- a/submodules/TelegramCore/Sources/ManagedLocalInputActivities.swift +++ b/submodules/TelegramCore/Sources/ManagedLocalInputActivities.swift @@ -7,12 +7,18 @@ import MtProtoKit import SyncCore public struct PeerActivitySpace: Hashable { - public var peerId: PeerId - public var threadId: Int64? + public enum Category: Equatable, Hashable { + case global + case thread(Int64) + case voiceChat + } - public init(peerId: PeerId, threadId: Int64?) { + public var peerId: PeerId + public var category: Category + + public init(peerId: PeerId, category: Category) { self.peerId = peerId - self.threadId = threadId + self.category = category } } @@ -83,7 +89,14 @@ func managedLocalTypingActivities(activities: Signal<[PeerActivitySpace: [PeerId } for (peerId, activity, disposable) in start { - disposable.set(requestActivity(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId.peerId, threadId: peerId.threadId, activity: activity?.activity).start()) + var threadId: Int64? + switch peerId.category { + case let .thread(id): + threadId = id + default: + break + } + disposable.set(requestActivity(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId.peerId, threadId: threadId, activity: activity?.activity).start()) } }) return ActionDisposable { diff --git a/submodules/TelegramCore/Sources/PeerInputActivityManager.swift b/submodules/TelegramCore/Sources/PeerInputActivityManager.swift index 7fa9888e0e..57db211664 100644 --- a/submodules/TelegramCore/Sources/PeerInputActivityManager.swift +++ b/submodules/TelegramCore/Sources/PeerInputActivityManager.swift @@ -46,7 +46,7 @@ private final class PeerInputActivityContext { record.timer.invalidate() var updateId = record.updateId var recordTimestamp = record.timestamp - if record.activity != activity || record.timestamp + 4.0 < timestamp { + if record.activity != activity || record.timestamp + 1.0 < timestamp { updated = true updateId = nextUpdateId recordTimestamp = timestamp @@ -329,7 +329,16 @@ final class PeerInputActivityManager { }) self.contexts[chatPeerId] = context } - context.addActivity(peerId: peerId, activity: activity, timeout: 8.0, episodeId: episodeId, nextUpdateId: &self.nextUpdateId) + + let timeout: Double + switch activity { + case .speakingInGroupCall: + timeout = 3.0 + default: + timeout = 8.0 + } + + context.addActivity(peerId: peerId, activity: activity, timeout: timeout, episodeId: episodeId, nextUpdateId: &self.nextUpdateId) if let globalContext = self.globalContext { let activities = self.collectActivities() @@ -381,7 +390,15 @@ final class PeerInputActivityManager { self?.addActivity(chatPeerId: chatPeerId, peerId: peerId, activity: activity, episodeId: episodeId) } - let timer = SignalKitTimer(timeout: 5.0, repeat: true, completion: { + let timeout: Double + switch activity { + case .speakingInGroupCall: + timeout = 2.0 + default: + timeout = 5.0 + } + + let timer = SignalKitTimer(timeout: timeout, repeat: true, completion: { update() }, queue: queue) timer.start() diff --git a/submodules/TelegramCore/Sources/PendingMessageManager.swift b/submodules/TelegramCore/Sources/PendingMessageManager.swift index 37645a7cd9..797d6a5598 100644 --- a/submodules/TelegramCore/Sources/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/PendingMessageManager.swift @@ -554,7 +554,13 @@ public final class PendingMessageManager { for subscriber in messageContext.statusSubscribers.copyItems() { subscriber(messageContext.status, messageContext.error) } - self.addContextActivityIfNeeded(messageContext, peerId: PeerActivitySpace(peerId: id.peerId, threadId: threadId)) + let activityCategory: PeerActivitySpace.Category + if let threadId = threadId { + activityCategory = .thread(threadId) + } else { + activityCategory = .global + } + self.addContextActivityIfNeeded(messageContext, peerId: PeerActivitySpace(peerId: id.peerId, category: activityCategory)) let queue = self.queue @@ -624,7 +630,15 @@ public final class PendingMessageManager { for subscriber in context.statusSubscribers.copyItems() { subscriber(context.status, context.error) } - self.addContextActivityIfNeeded(context, peerId: PeerActivitySpace(peerId: peerId, threadId: context.threadId)) + + let activityCategory: PeerActivitySpace.Category + if let threadId = context.threadId { + activityCategory = .thread(threadId) + } else { + activityCategory = .global + } + + self.addContextActivityIfNeeded(context, peerId: PeerActivitySpace(peerId: peerId, category: activityCategory)) context.uploadDisposable.set((uploadSignal |> deliverOn(self.queue)).start(next: { [weak self] next in if let strongSelf = self { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a981abcb77..d9689bbc88 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3167,9 +3167,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let activitySpace: PeerActivitySpace switch self.chatLocation { case let .peer(peerId): - activitySpace = PeerActivitySpace(peerId: peerId, threadId: nil) + activitySpace = PeerActivitySpace(peerId: peerId, category: .global) case let .replyThread(replyThreadMessage): - activitySpace = PeerActivitySpace(peerId: replyThreadMessage.messageId.peerId, threadId: makeMessageThreadId(replyThreadMessage.messageId)) + activitySpace = PeerActivitySpace(peerId: replyThreadMessage.messageId.peerId, category: .thread(makeMessageThreadId(replyThreadMessage.messageId))) } self.inputActivityDisposable = (self.typingActivityPromise.get() @@ -6074,11 +6074,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let postbox = self.context.account.postbox let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) - var activityThreadId: Int64? + var activityCategory: PeerActivitySpace.Category = .global if case let .replyThread(replyThreadMessage) = self.chatLocation { - activityThreadId = makeMessageThreadId(replyThreadMessage.messageId) + activityCategory = .thread(makeMessageThreadId(replyThreadMessage.messageId)) } - self.peerInputActivitiesDisposable = (self.context.account.peerInputActivities(peerId: PeerActivitySpace(peerId: peerId, threadId: activityThreadId)) + self.peerInputActivitiesDisposable = (self.context.account.peerInputActivities(peerId: PeerActivitySpace(peerId: peerId, category: activityCategory)) |> mapToSignal { activities -> Signal<[(Peer, PeerInputActivity)], NoError> in var foundAllPeers = true var cachedResult: [(Peer, PeerInputActivity)] = [] diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 1a3df12fbe..353184e3e5 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 1a3df12fbe943fe66df86607a5e6347a0db4cf4f +Subproject commit 353184e3e5c5e560a59cb836ca8de496c0cc95bb diff --git a/third-party/webrtc/webrtc-ios b/third-party/webrtc/webrtc-ios index 7198385cd3..178f6fb925 160000 --- a/third-party/webrtc/webrtc-ios +++ b/third-party/webrtc/webrtc-ios @@ -1 +1 @@ -Subproject commit 7198385cd356994e366ea325c84b34e974c9117e +Subproject commit 178f6fb9253851f3c363b2deed0cba433973862b