This commit is contained in:
Ali 2020-11-27 22:12:59 +04:00
parent bc0272e224
commit 1db12483cc
19 changed files with 466 additions and 129 deletions

View File

@ -185,30 +185,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
}
}
public struct PresentationGroupCallMemberState: Equatable {
public var ssrc: UInt32
public var muteState: GroupCallParticipantsContext.Participant.MuteState?
public init(
ssrc: UInt32,
muteState: GroupCallParticipantsContext.Participant.MuteState?
) {
self.ssrc = ssrc
self.muteState = muteState
self.numberOfActiveSpeakers = numberOfActiveSpeakers
}
}
@ -217,6 +207,22 @@ public enum PresentationGroupCallMuteAction: Equatable {
case unmuted
}
public struct PresentationGroupCallMembers: Equatable {
public var participants: [GroupCallParticipantsContext.Participant]
public var totalCount: Int
public var loadMoreToken: String?
public init(
participants: [GroupCallParticipantsContext.Participant],
totalCount: Int,
loadMoreToken: String?
) {
self.participants = participants
self.totalCount = totalCount
self.loadMoreToken = loadMoreToken
}
}
public protocol PresentationGroupCall: class {
var account: Account { get }
var accountContext: AccountContext { get }
@ -228,7 +234,7 @@ public protocol PresentationGroupCall: class {
var canBeRemoved: Signal<Bool, NoError> { get }
var state: Signal<PresentationGroupCallState, NoError> { get }
var summaryState: Signal<PresentationGroupCallSummaryState?, NoError> { get }
var members: Signal<[PeerId: PresentationGroupCallMemberState], NoError> { get }
var members: Signal<PresentationGroupCallMembers?, NoError> { get }
var audioLevels: Signal<[(PeerId, Float)], NoError> { get }
var myAudioLevel: Signal<Float, NoError> { get }
var isMuted: Signal<Bool, NoError> { get }

View File

@ -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

View File

@ -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)] = []

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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 {

View File

@ -225,15 +225,28 @@ 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)
}
@ -250,15 +263,27 @@ 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 {

View File

@ -36,6 +36,7 @@ final class GroupCallPanelData {
let info: GroupCallInfo
let topParticipants: [GroupCallParticipantsContext.Participant]
let participantCount: Int
let numberOfActiveSpeakers: Int
let groupCall: PresentationGroupCall?
init(
@ -43,12 +44,14 @@ 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
}
}
@ -312,6 +315,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
info: summary.info,
topParticipants: summary.topParticipants,
participantCount: summary.participantCount,
numberOfActiveSpeakers: 0,
groupCall: call
)
}
@ -331,21 +335,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<GroupCallSummary?, NoError> in
return getGroupCallParticipants(account: context.account, callId: activeCall.id, accessHash: activeCall.accessHash, offset: "", limit: 10)
|> map(Optional.init)
|> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in
return .single(nil)
}
|> map { summary -> GroupCallPanelData? in
guard let summary = summary else {
return nil
|> mapToSignal { initialState -> Signal<GroupCallPanelData?, NoError> 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<GroupCallPanelData?, NoError> { subscriber in
let participantsContext = QueueLocalObject<GroupCallParticipantsContext>(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 {

View File

@ -56,13 +56,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
}
}
@ -139,6 +142,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var audioSessionActiveDisposable: Disposable?
private var isAudioSessionActive = false
private let typingDisposable = MetaDisposable()
private let _canBeRemoved = Promise<Bool>(false)
public var canBeRemoved: Signal<Bool, NoError> {
return self._canBeRemoved.get()
@ -156,15 +161,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<PresentationGroupCallMembers?>(nil)
public var members: Signal<PresentationGroupCallMembers?, NoError> {
return self.membersPromise.get()
}
@ -191,6 +196,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var checkCallDisposable: Disposable?
private var isCurrentlyConnecting: Bool?
private var myAudioLevelTimer: SwiftSignalKit.Timer?
init(
accountContext: AccountContext,
audioSession: ManagedAudioSession,
@ -329,7 +336,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
info: infoState.info,
participantCount: participantsState.participantCount,
callState: callState,
topParticipants: participantsState.topParticipants
topParticipants: participantsState.topParticipants,
numberOfActiveSpeakers: participantsState.numberOfActiveSpeakers
)
})
@ -351,6 +359,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?) {
@ -457,7 +468,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)
}))
}
}
@ -481,31 +496,36 @@ 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((participantsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(),
participantsContext.state,
participantsContext.numberOfActiveSpeakers
).start(next: { [weak self] state, numberOfActiveSpeakers in
guard let strongSelf = self else {
return
}
var memberStates: [PeerId: PresentationGroupCallMemberState] = [:]
var topParticipants: [GroupCallParticipantsContext.Participant] = []
var members = PresentationGroupCallMembers(
participants: [],
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
)
if participant.peer.id == strongSelf.accountContext.account.peerId {
if let muteState = participant.muteState {
strongSelf.stateValue.muteState = muteState
@ -516,13 +536,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
)))
}))
@ -739,4 +764,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()
}
}
}
}

View File

@ -243,8 +243,8 @@ 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 currentInvitedPeers: Set<PeerId>?
private var currentEntries: [PeerEntry] = []
@ -426,19 +426,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 ?? [], 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, invitedPeers: strongSelf.currentInvitedPeers ?? Set())
} else {
strongSelf.currentMemberStates = memberStates
strongSelf.currentCallMembers = callMembers.participants
}
})
@ -447,8 +447,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 ?? [], invitedPeers: invitedPeers)
} else {
strongSelf.currentInvitedPeers = invitedPeers
}
@ -510,8 +510,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 ?? [], invitedPeers: strongSelf.currentInvitedPeers ?? Set())
}
if let (layout, navigationHeight) = strongSelf.validLayout {
@ -964,28 +964,42 @@ public final class VoiceChatController: ViewController {
})
}
private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, members: [RenderedChannelParticipant], memberStates: [PeerId: PresentationGroupCallMemberState], invitedPeers: Set<PeerId>) {
var members = members
members.sort(by: { lhs, rhs in
private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, groupMembers: [RenderedChannelParticipant], callMembers: [GroupCallParticipantsContext.Participant], invitedPeers: Set<PeerId>) {
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.currentInvitedPeers = invitedPeers
let previousEntries = self.currentEntries
@ -993,7 +1007,44 @@ public final class VoiceChatController: ViewController {
var index: Int32 = 0
for member in members {
var processedPeerIds = Set<PeerId>()
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 = .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
}
@ -1006,9 +1057,6 @@ public final class VoiceChatController: ViewController {
} else {
memberState = .listening
}
} else if let state = memberStates[member.peer.id] {
memberState = .listening
memberMuteState = state.muteState
} else {
memberState = .inactive
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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 {
@ -385,7 +397,11 @@ public func leaveGroupCall(account: Account, callId: Int64, accessHash: Int64, s
|> mapError { _ -> LeaveGroupCallError in
return .generic
}
|> ignoreValues
|> mapToSignal { result -> Signal<Never, LeaveGroupCallError> 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<Int>(0)
public var numberOfActiveSpeakers: Signal<Int, NoError> {
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<InternalState>(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 {

View File

@ -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 {

View File

@ -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()

View File

@ -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 {

View File

@ -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)] = []

@ -1 +1 @@
Subproject commit c03d265224f18997ebc969753f7922de7a089b95
Subproject commit 10d63157e4ebdf71d1ea3694bef2ab1f7d7f033c