diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 96b34f56d0..1d7a93b010 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -272,7 +272,7 @@ public protocol PresentationGroupCall: class { var state: Signal { get } var summaryState: Signal { get } var members: Signal { get } - var audioLevels: Signal<[(PeerId, Float)], NoError> { get } + var audioLevels: Signal<[(PeerId, Float, Bool)], NoError> { get } var myAudioLevel: Signal { get } var isMuted: Signal { get } @@ -285,7 +285,7 @@ public protocol PresentationGroupCall: class { func updateMuteState(peerId: PeerId, isMuted: Bool) - func invitePeer(_ peerId: PeerId) + func invitePeer(_ peerId: PeerId) -> Bool func removedPeer(_ peerId: PeerId) var invitedPeers: Signal<[PeerId], NoError> { get } } diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index dc54f10713..e2a10577aa 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -1010,9 +1010,9 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi if updateFlags == nil { if member.adminInfo?.rights == nil { if channel.flags.contains(.isCreator) { - updateFlags = maskRightsFlags.subtracting(.canAddAdmins) + updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous]) } else if let adminRights = channel.adminRights { - updateFlags = maskRightsFlags.intersection(adminRights.flags).subtracting(.canAddAdmins) + updateFlags = maskRightsFlags.intersection(adminRights.flags).subtracting([.canAddAdmins, .canBeAnonymous]) } else { updateFlags = [] } diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 05f5801db0..be960e4111 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -239,20 +239,20 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } - private let audioLevelsPromise = Promise<[(PeerId, Float)]>() + private let audioLevelsPromise = Promise<[(PeerId, Float, Bool)]>() init() { } - func update(levels: [(PeerId, Float)]) { + func update(levels: [(PeerId, Float, Bool)]) { let timestamp = Int32(CFAbsoluteTimeGetCurrent()) let currentParticipants: [PeerId: Participant] = self.participants var validSpeakers: [PeerId: Participant] = [:] var silentParticipants = Set() var speakingParticipants = Set() - for (peerId, level) in levels { - if level > speakingLevelThreshold { + for (peerId, level, hasVoice) in levels { + if level > speakingLevelThreshold && hasVoice { validSpeakers[peerId] = Participant(timestamp: timestamp, level: level) speakingParticipants.insert(peerId) } else { @@ -276,9 +276,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } } - var audioLevels: [(PeerId, Float)] = [] - for (peerId, speaker) in validSpeakers { - audioLevels.append((peerId, speaker.level)) + var audioLevels: [(PeerId, Float, Bool)] = [] + for (peerId, level, hasVoice) in levels { + if level > 0.1 { + audioLevels.append((peerId, level, hasVoice)) + } } self.participants = validSpeakers @@ -290,7 +292,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { return self.speakingParticipantsPromise.get() |> distinctUntilChanged } - func getAudioLevels() -> Signal<[(PeerId, Float)], NoError> { + func getAudioLevels() -> Signal<[(PeerId, Float, Bool)], NoError> { return self.audioLevelsPromise.get() } } @@ -310,6 +312,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { public let peerId: PeerId public let peer: Peer? + private let temporaryJoinTimestamp: Int32 + private var internalState: InternalState = .requesting private var callContext: OngoingGroupCallContext? @@ -357,7 +361,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { private let speakingParticipantsContext = SpeakingParticipantsContext() private var speakingParticipantsReportTimestamp: [PeerId: Double] = [:] - public var audioLevels: Signal<[(PeerId, Float)], NoError> { + public var audioLevels: Signal<[(PeerId, Float, Bool)], NoError> { return self.speakingParticipantsContext.getAudioLevels() } @@ -459,6 +463,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.peerId = peerId self.peer = peer + self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + var didReceiveAudioOutputs = false if !audioSession.getIsHeadsetPluggedIn() { @@ -611,10 +617,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { impl.get(account: accountContext.account, peerId: peerId, call: CachedChannelData.ActiveCall(id: initialCall.id, accessHash: initialCall.accessHash)) }) { if let participantsContext = temporaryParticipantsContext.context.participantsContext { + let accountPeerId = self.accountContext.account.peerId + let accountPeer = self.accountContext.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(accountPeerId) + } self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(), + accountPeer, participantsContext.state, participantsContext.activeSpeakers - ).start(next: { [weak self] state, activeSpeakers in + ).start(next: { [weak self] accountPeer, state, activeSpeakers in guard let strongSelf = self else { return } @@ -631,7 +642,22 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { var updatedInvitedPeers = strongSelf.invitedPeersValue var didUpdateInvitedPeers = false - for participant in state.participants { + var participants = state.participants + + if !participants.contains(where: { $0.peer.id == accountPeerId }) { + if let accountPeer = accountPeer { + participants.append(GroupCallParticipantsContext.Participant( + peer: accountPeer, + ssrc: 0, + joinTimestamp: strongSelf.temporaryJoinTimestamp, + activityTimestamp: nil, + muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true) + )) + participants.sort() + } + } + + for participant in participants { members.participants.append(participant) if topParticipants.count < 3 { @@ -816,9 +842,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { guard let strongSelf = self else { return } - var result: [(PeerId, Float)] = [] + var result: [(PeerId, Float, Bool)] = [] var myLevel: Float = 0.0 - for (ssrcKey, level) in levels { + var myLevelHasVoice: Bool = false + for (ssrcKey, level, hasVoice) in levels { var peerId: PeerId? switch ssrcKey { case .local: @@ -828,20 +855,20 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } if let peerId = peerId { if case .local = ssrcKey { - myLevel = level + if !strongSelf.isMutedValue.isEffectivelyMuted { + myLevel = level + myLevelHasVoice = hasVoice + } } - result.append((peerId, level)) + result.append((peerId, level, hasVoice)) } } + strongSelf.speakingParticipantsContext.update(levels: result) let mappedLevel = myLevel * 1.5 - strongSelf.myAudioLevelPipe.putNext(mappedLevel) - strongSelf.processMyAudioLevel(level: mappedLevel) - if !strongSelf.isMutedValue.isEffectivelyMuted { - strongSelf.speakingParticipantsContext.update(levels: [(strongSelf.account.peerId, mappedLevel)]) - } + strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice) })) } } @@ -1273,9 +1300,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { })) } - public func invitePeer(_ peerId: PeerId) { + public func invitePeer(_ peerId: PeerId) -> Bool { guard case let .estabilished(callInfo, _, _, _) = self.internalState, !self.invitedPeersValue.contains(peerId) else { - return + return false } var updatedInvitedPeers = self.invitedPeersValue @@ -1283,6 +1310,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.invitedPeersValue = updatedInvitedPeers let _ = inviteToGroupCall(account: self.account, callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start() + + return true } public func removedPeer(_ peerId: PeerId) { @@ -1329,10 +1358,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { myAudioLevelTimer.start() } - private func processMyAudioLevel(level: Float) { + private func processMyAudioLevel(level: Float, hasVoice: Bool) { self.currentMyAudioLevel = level - if level > 0.01 { + if level > 0.01 && hasVoice { self.currentMyAudioLevelTimestamp = CACurrentMediaTime() if self.myAudioLevelTimer == nil { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 86fe81ef98..694d26e9e6 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -172,9 +172,9 @@ public final class VoiceChatController: ViewController { } } - func updateAudioLevels(_ levels: [(PeerId, Float)], reset: Bool = false) { + func updateAudioLevels(_ levels: [(PeerId, Float, Bool)], reset: Bool = false) { var updated = Set() - for (peerId, level) in levels { + for (peerId, level, _) in levels { if let pipe = self.audioLevels[peerId] { if reset { pipe.putNext(level) @@ -578,10 +578,11 @@ public final class VoiceChatController: ViewController { return } if let participant = participant { - strongSelf.call.invitePeer(participant.peer.id) dismissController?() - strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: participant.peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), in: .current) + if strongSelf.call.invitePeer(participant.peer.id) { + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: participant.peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), in: .current) + } } else { strongSelf.controller?.present(textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), groupPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { guard let strongSelf = self else { @@ -649,10 +650,11 @@ public final class VoiceChatController: ViewController { dismissController?() return } - strongSelf.call.invitePeer(peer.id) dismissController?() - strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), in: .current) + if strongSelf.call.invitePeer(peer.id) { + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0), elevatedLayout: false, action: { _ in return false }), in: .current) + } })) })]), in: .window(.root)) } @@ -846,7 +848,6 @@ public final class VoiceChatController: ViewController { } if strongSelf.callState != state { - let wasMuted = strongSelf.callState?.muteState != nil strongSelf.callState = state if let muteState = state.muteState, !muteState.canUnmute { @@ -1214,7 +1215,7 @@ public final class VoiceChatController: ViewController { self.call.setIsMuted(action: .muted(isPushToTalkActive: false)) } - self.itemInteraction?.updateAudioLevels([(self.context.account.peerId, 0.0)], reset: true) + self.itemInteraction?.updateAudioLevels([(self.context.account.peerId, 0.0, false)], reset: true) if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) @@ -1744,17 +1745,6 @@ public final class VoiceChatController: ViewController { index += 1 } - if let accountPeer = self.accountPeer, !processedPeerIds.contains(accountPeer.id) { - entries.insert(.peer(PeerEntry( - peer: accountPeer, - presence: nil, - activityTimestamp: Int32.max - 1 - index, - state: .listening, - muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true), - canManageCall: callState?.canManageCall ?? false - )), at: 1) - } - for peer in invitedPeers { if processedPeerIds.contains(peer.id) { continue diff --git a/submodules/TelegramCore/Sources/GroupCalls.swift b/submodules/TelegramCore/Sources/GroupCalls.swift index 53139fa6cc..ce1fc05af1 100644 --- a/submodules/TelegramCore/Sources/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/GroupCalls.swift @@ -103,6 +103,7 @@ public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int peer: peer, ssrc: ssrc, joinTimestamp: date, + activityTimestamp: activeDate.flatMap(Double.init), muteState: muteState )) } @@ -512,6 +513,20 @@ public final class GroupCallParticipantsContext { public var activityTimestamp: Double? public var muteState: MuteState? + public init( + peer: Peer, + ssrc: UInt32, + joinTimestamp: Int32, + activityTimestamp: Double?, + muteState: MuteState? + ) { + self.peer = peer + self.ssrc = ssrc + self.joinTimestamp = joinTimestamp + self.activityTimestamp = activityTimestamp + self.muteState = muteState + } + public static func ==(lhs: Participant, rhs: Participant) -> Bool { if !lhs.peer.isEqual(rhs.peer) { return false diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index df939562de..5ead890d83 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -235,7 +235,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private let unblockingPeer = ValuePromise(false, ignoreRepeated: true) private let searching = ValuePromise(false, ignoreRepeated: true) private let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>() - private let loadingMessage = ValuePromise(nil, ignoreRepeated: true) + private let loadingMessage = Promise(nil) private let performingInlineSearch = ValuePromise(false, ignoreRepeated: true) private var preloadHistoryPeerId: PeerId? @@ -4828,7 +4828,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - strongSelf.loadingMessage.set(.generic) + strongSelf.loadingMessage.set(.single(.generic)) let peerId: PeerId let threadId: Int64? @@ -4843,7 +4843,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.messageIndexDisposable.set((searchMessageIdByTimestamp(account: strongSelf.context.account, peerId: peerId, threadId: threadId, timestamp: timestamp) |> deliverOnMainQueue).start(next: { messageId in if let strongSelf = self { - strongSelf.loadingMessage.set(nil) + strongSelf.loadingMessage.set(.single(nil)) if let messageId = messageId { strongSelf.navigateToMessage(from: nil, to: .id(messageId), forceInCurrentChat: true) } @@ -9243,13 +9243,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, completed: { [weak self] in if let strongSelf = self { - strongSelf.loadingMessage.set(nil) + strongSelf.loadingMessage.set(.single(nil)) strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() } })) cancelImpl = { [weak self] in if let strongSelf = self { - strongSelf.loadingMessage.set(nil) + strongSelf.loadingMessage.set(.single(nil)) strongSelf.messageIndexDisposable.set(nil) } } @@ -9307,13 +9307,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, completed: { [weak self] in if let strongSelf = self { - strongSelf.loadingMessage.set(nil) + strongSelf.loadingMessage.set(.single(nil)) strongSelf.chatDisplayNode.historyNode.scrollToStartOfHistory() } })) cancelImpl = { [weak self] in if let strongSelf = self { - strongSelf.loadingMessage.set(nil) + strongSelf.loadingMessage.set(.single(nil)) strongSelf.messageIndexDisposable.set(nil) } } @@ -9491,14 +9491,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let scrollFromIndex = scrollFromIndex { if let messageId = messageLocation.messageId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - self.loadingMessage.set(nil) + self.loadingMessage.set(.single(nil)) self.messageIndexDisposable.set(nil) self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, scrollPosition: scrollPosition) completion?() } else if case let .index(index) = messageLocation, index.id.id == 0, index.timestamp > 0, case .scheduledMessages = self.presentationInterfaceState.subject { self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition) } else { - self.loadingMessage.set(statusSubject) + self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) let searchLocation: ChatHistoryInitialSearchLocation switch messageLocation { case let .id(id): @@ -9580,12 +9580,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, completed: { [weak self] in if let strongSelf = self { - strongSelf.loadingMessage.set(nil) + strongSelf.loadingMessage.set(.single(nil)) } })) cancelImpl = { [weak self] in if let strongSelf = self { - strongSelf.loadingMessage.set(nil) + strongSelf.loadingMessage.set(.single(nil)) strongSelf.messageIndexDisposable.set(nil) } } @@ -9607,7 +9607,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let _ = fromId, rememberInStack { self.historyNavigationStack.add(fromIndex) } - self.loadingMessage.set(statusSubject) + self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(location: searchLocation, count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let signal = historyView |> mapToSignal { historyView -> Signal in @@ -9638,7 +9638,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, completed: { [weak self] in if let strongSelf = self { - strongSelf.loadingMessage.set(nil) + strongSelf.loadingMessage.set(.single(nil)) } })) } else { diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index ecd0720efe..3477df3b60 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -46,7 +46,7 @@ public final class OngoingGroupCallContext { let joinPayload = Promise<(String, UInt32)>() let networkState = ValuePromise(.connecting, ignoreRepeated: true) let isMuted = ValuePromise(true, ignoreRepeated: true) - let audioLevels = ValuePipe<[(AudioLevelKey, Float)]>() + let audioLevels = ValuePipe<[(AudioLevelKey, Float, Bool)]>() init(queue: Queue, inputDeviceId: String, outputDeviceId: String) { self.queue = queue @@ -88,7 +88,7 @@ public final class OngoingGroupCallContext { let audioLevels = self.audioLevels audioLevelsUpdatedImpl = { levels in - var mappedLevels: [(AudioLevelKey, Float)] = [] + var mappedLevels: [(AudioLevelKey, Float, Bool)] = [] var i = 0 while i < levels.count { let uintValue = levels[i].uint32Value @@ -98,8 +98,8 @@ public final class OngoingGroupCallContext { } else { key = .source(uintValue) } - mappedLevels.append((key, levels[i + 1].floatValue)) - i += 2 + mappedLevels.append((key, levels[i + 1].floatValue, levels[i + 2].boolValue)) + i += 3 } queue.async { audioLevels.putNext(mappedLevels) @@ -177,7 +177,7 @@ public final class OngoingGroupCallContext { } } - public var audioLevels: Signal<[(AudioLevelKey, Float)], NoError> { + public var audioLevels: Signal<[(AudioLevelKey, Float, Bool)], NoError> { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 122701ba85..88b8a21c8c 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -825,11 +825,12 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL; networkStateUpdated(isConnected ? GroupCallNetworkStateConnected : GroupCallNetworkStateConnecting); }]; }, - .audioLevelsUpdated = [audioLevelsUpdated](std::vector> const &levels) { + .audioLevelsUpdated = [audioLevelsUpdated](std::vector>> const &levels) { NSMutableArray *result = [[NSMutableArray alloc] init]; for (auto &it : levels) { [result addObject:@(it.first)]; - [result addObject:@(it.second)]; + [result addObject:@(it.second.first)]; + [result addObject:@(it.second.second)]; } audioLevelsUpdated(result); }, diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 503f8ad1e8..91102cef8e 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 503f8ad1e8f0a744b1fb3eddb66e33176e427074 +Subproject commit 91102cef8ef03b7223b04e28c8794c87bc78614d