Call Quality Survey
This commit is contained in:
parent
e5974d7e59
commit
c9e9f4142c
@ -1720,12 +1720,14 @@
|
||||
B95967842D52976A00F9A800 /* NavigationPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95967832D52976A00F9A800 /* NavigationPreviewController.swift */; };
|
||||
B95A765C2B76C5BB00AA7E97 /* AvatarViewPresentationContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95A765B2B76C5BB00AA7E97 /* AvatarViewPresentationContextProvider.swift */; };
|
||||
B95A765E2B76E93500AA7E97 /* FindByUsernameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95A765D2B76E93500AA7E97 /* FindByUsernameViewController.swift */; };
|
||||
B965716E2F0DC91900690CE9 /* CallQualitySurvey.swift in Sources */ = {isa = PBXBuildFile; fileRef = B965716C2F0CAA3600690CE9 /* CallQualitySurvey.swift */; };
|
||||
B96D6D792B9F83270039EB99 /* SignalSymbols-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = B96D6D782B9F83270039EB99 /* SignalSymbols-Regular.otf */; };
|
||||
B9754F542C73AD49000000E4 /* ConversationAvatarView+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9754F532C73AD49000000E4 /* ConversationAvatarView+SwiftUI.swift */; };
|
||||
B97803012DD65B0A00E9FC82 /* LinearProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97803002DD65B0A00E9FC82 /* LinearProgressView.swift */; };
|
||||
B982ACFF2BA8FD2A00AD7E81 /* SignalSymbols-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = B982ACFE2BA8FD2A00AD7E81 /* SignalSymbols-Bold.otf */; };
|
||||
B982AD012BA8FD3200AD7E81 /* SignalSymbols-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = B982AD002BA8FD3100AD7E81 /* SignalSymbols-Light.otf */; };
|
||||
B985D29A2EE7E58700E3BF5F /* CallQualitySurveyCustomIssueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B985D2992EE7E58700E3BF5F /* CallQualitySurveyCustomIssueViewController.swift */; };
|
||||
B985D29E2EEA913000E3BF5F /* CallQualitySurvey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = B985D29D2EEA913000E3BF5F /* CallQualitySurvey.pb.swift */; };
|
||||
B9921F882CC6FDB200AB667F /* HeroSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9921F872CC6FDB200AB667F /* HeroSheetViewController.swift */; };
|
||||
B99287FB2CF0FE8D000D62C4 /* LinkedDevicesEducationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99287FA2CF0FE8D000D62C4 /* LinkedDevicesEducationSheet.swift */; };
|
||||
B99288002CF124AC000D62C4 /* Text+Links.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99287FF2CF124AC000D62C4 /* Text+Links.swift */; };
|
||||
@ -5909,6 +5911,7 @@
|
||||
B95A765B2B76C5BB00AA7E97 /* AvatarViewPresentationContextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarViewPresentationContextProvider.swift; sourceTree = "<group>"; };
|
||||
B95A765D2B76E93500AA7E97 /* FindByUsernameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindByUsernameViewController.swift; sourceTree = "<group>"; };
|
||||
B95BBAC12BB36025009EFB4A /* ProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileName.swift; sourceTree = "<group>"; };
|
||||
B965716C2F0CAA3600690CE9 /* CallQualitySurvey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallQualitySurvey.swift; sourceTree = "<group>"; };
|
||||
B96D6D782B9F83270039EB99 /* SignalSymbols-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SignalSymbols-Regular.otf"; sourceTree = "<group>"; };
|
||||
B96FEE2E2CDC297500836191 /* User.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = User.xcconfig; sourceTree = "<group>"; };
|
||||
B9754F532C73AD49000000E4 /* ConversationAvatarView+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationAvatarView+SwiftUI.swift"; sourceTree = "<group>"; };
|
||||
@ -5916,6 +5919,8 @@
|
||||
B982ACFE2BA8FD2A00AD7E81 /* SignalSymbols-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SignalSymbols-Bold.otf"; sourceTree = "<group>"; };
|
||||
B982AD002BA8FD3100AD7E81 /* SignalSymbols-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SignalSymbols-Light.otf"; sourceTree = "<group>"; };
|
||||
B985D2992EE7E58700E3BF5F /* CallQualitySurveyCustomIssueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallQualitySurveyCustomIssueViewController.swift; sourceTree = "<group>"; };
|
||||
B985D29C2EEA8FE700E3BF5F /* CallQualitySurvey.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = CallQualitySurvey.proto; sourceTree = "<group>"; };
|
||||
B985D29D2EEA913000E3BF5F /* CallQualitySurvey.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallQualitySurvey.pb.swift; sourceTree = "<group>"; };
|
||||
B9921F872CC6FDB200AB667F /* HeroSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeroSheetViewController.swift; sourceTree = "<group>"; };
|
||||
B99287FA2CF0FE8D000D62C4 /* LinkedDevicesEducationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedDevicesEducationSheet.swift; sourceTree = "<group>"; };
|
||||
B99287FF2CF124AC000D62C4 /* Text+Links.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+Links.swift"; sourceTree = "<group>"; };
|
||||
@ -13636,6 +13641,7 @@
|
||||
D9D321792A8FEA7A004FC110 /* Specifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B985D29C2EEA8FE700E3BF5F /* CallQualitySurvey.proto */,
|
||||
D9D3217C2A8FEA9B004FC110 /* DeviceTransfer.proto */,
|
||||
D9D3217D2A8FEA9C004FC110 /* Fingerprint.proto */,
|
||||
D9D3217B2A8FEA9B004FC110 /* Groups.proto */,
|
||||
@ -13757,6 +13763,7 @@
|
||||
D9E43BCC2CC194140001536E /* CallLinkManager.swift */,
|
||||
D9E43BCD2CC194140001536E /* CallLinkStateUpdater.swift */,
|
||||
D9E43BCE2CC194140001536E /* CallLinkUpdateMessageSender.swift */,
|
||||
B965716C2F0CAA3600690CE9 /* CallQualitySurvey.swift */,
|
||||
D9E43BCF2CC194140001536E /* CallRecordLoader.swift */,
|
||||
D9E43BD02CC194140001536E /* CallService.swift */,
|
||||
D9E43BD12CC194140001536E /* CallServiceState.swift */,
|
||||
@ -14715,6 +14722,7 @@
|
||||
F9C5C9A2289453B100548EEE /* Generated */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B985D29D2EEA913000E3BF5F /* CallQualitySurvey.pb.swift */,
|
||||
F9C5C9A9289453B100548EEE /* DeviceTransfer.pb.swift */,
|
||||
F9C5C9B5289453B100548EEE /* DeviceTransferProto.swift */,
|
||||
F9C5C9A6289453B100548EEE /* Fingerprint.pb.swift */,
|
||||
@ -17799,6 +17807,7 @@
|
||||
D9E43BF32CC194140001536E /* CallMemberVideoView.swift in Sources */,
|
||||
D9E43BF42CC194140001536E /* CallMemberView.swift in Sources */,
|
||||
D9E43BF52CC194140001536E /* CallMemberWaitingAndErrorView.swift in Sources */,
|
||||
B965716E2F0DC91900690CE9 /* CallQualitySurvey.swift in Sources */,
|
||||
B985D29A2EE7E58700E3BF5F /* CallQualitySurveyCustomIssueViewController.swift in Sources */,
|
||||
B924DFAC2EE7C8E000B88147 /* CallQualitySurveyDebugLogViewController.swift in Sources */,
|
||||
B924DFAA2EE7C6F900B88147 /* CallQualitySurveyIssuesViewController.swift in Sources */,
|
||||
@ -18742,6 +18751,7 @@
|
||||
50C831762BAA3A8000BEBF25 /* CallMessageHandler.swift in Sources */,
|
||||
72C9058E2B9AC8E600E586B8 /* CallMessageRelay.swift in Sources */,
|
||||
5021B0332C0106470028AC87 /* CallOfferHandler.swift in Sources */,
|
||||
B985D29E2EEA913000E3BF5F /* CallQualitySurvey.pb.swift in Sources */,
|
||||
D9C544292B8578B50036F274 /* CallRecord+CallStatus.swift in Sources */,
|
||||
D9E7C8772B9A4A9C005BD3B9 /* CallRecord+Sorting.swift in Sources */,
|
||||
D99A2A852AAB9AB9003388D1 /* CallRecord.swift in Sources */,
|
||||
|
||||
265
Signal/Calls/CallQualitySurvey.swift
Normal file
265
Signal/Calls/CallQualitySurvey.swift
Normal file
@ -0,0 +1,265 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SignalRingRTC
|
||||
import SignalServiceKit
|
||||
|
||||
enum CallQualitySurvey {
|
||||
enum CallType: String {
|
||||
case individualAudio = "direct_voice"
|
||||
case individualVideo = "direct_video"
|
||||
case group = "group"
|
||||
case link = "call_link"
|
||||
}
|
||||
|
||||
enum Rating {
|
||||
case satisfied
|
||||
case hadIssues(Set<Issue>, customIssue: String?)
|
||||
}
|
||||
|
||||
enum Issue: String {
|
||||
case audio = "audio"
|
||||
case audioStuttering = "audio_stuttering"
|
||||
case audioLocalEcho = "audio_local_echo"
|
||||
case audioRemoteEcho = "audio_remote_echo"
|
||||
case audioDrop = "audio_drop"
|
||||
case video = "video"
|
||||
case videoNoCamera = "video_no_camera"
|
||||
case videoLowQuality = "video_low_quality"
|
||||
case videoLowResolution = "video_low_resolution"
|
||||
case callDropped = "call_dropped"
|
||||
case other = "other"
|
||||
}
|
||||
}
|
||||
|
||||
class CallQualitySurveyManager {
|
||||
private typealias Proto = CallQualitySurveyProtos_SubmitCallQualitySurveyRequest
|
||||
|
||||
private enum StoreKeys {
|
||||
static let lastFailureSubmittedDate = "lastFailureSubmittedDate"
|
||||
static let lastPromptDate = "lastPromptDate"
|
||||
}
|
||||
|
||||
private let kvStore = NewKeyValueStore(collection: "CallQualitySurveyStore")
|
||||
private let logger = PrefixedLogger(prefix: "[CallQualitySurvey]")
|
||||
|
||||
struct Deps {
|
||||
let db: DB
|
||||
let accountManager: TSAccountManager
|
||||
let networkManager: NetworkManager
|
||||
}
|
||||
|
||||
private let deps: Deps
|
||||
|
||||
private let callSummary: CallSummary
|
||||
private let callType: CallQualitySurvey.CallType
|
||||
|
||||
init(
|
||||
callSummary: CallSummary,
|
||||
callType: CallQualitySurvey.CallType,
|
||||
deps: Deps,
|
||||
) {
|
||||
self.callSummary = callSummary
|
||||
self.callType = callType
|
||||
self.deps = deps
|
||||
}
|
||||
|
||||
func showIfNeeded() {
|
||||
guard deps.db.read(block: shouldShowSurvey(tx:)) else { return }
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
UIApplication.shared.frontmostViewController?.present(
|
||||
CallQualitySurveyNavigationController(callQualitySurveyManager: self),
|
||||
animated: true,
|
||||
)
|
||||
|
||||
self.deps.db.write { tx in
|
||||
self.kvStore.writeValue(
|
||||
Date(),
|
||||
forKey: StoreKeys.lastPromptDate,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldShowSurvey(tx: DBReadTransaction) -> Bool {
|
||||
if InMemorySettings.forceCallQualitySurvey {
|
||||
return true
|
||||
}
|
||||
|
||||
guard callSummary.isSurveyCandidate else { return false }
|
||||
|
||||
let minimumTimeInterval: TimeInterval = .day
|
||||
|
||||
if callSummary.isFailure {
|
||||
if
|
||||
let lastFailureSubmittedDate = kvStore.fetchValue(
|
||||
Date.self,
|
||||
forKey: StoreKeys.lastFailureSubmittedDate,
|
||||
tx: tx,
|
||||
),
|
||||
lastFailureSubmittedDate.addingTimeInterval(minimumTimeInterval).isAfterNow
|
||||
{
|
||||
// Last failure was submitted within the past 24 hours
|
||||
return false
|
||||
}
|
||||
|
||||
// No failures have been submitted within the past 24 hours
|
||||
return true
|
||||
}
|
||||
|
||||
if
|
||||
let lastPromptDate = kvStore.fetchValue(
|
||||
Date.self,
|
||||
forKey: StoreKeys.lastPromptDate,
|
||||
tx: tx,
|
||||
),
|
||||
lastPromptDate.addingTimeInterval(minimumTimeInterval).isAfterNow
|
||||
{
|
||||
// Prompt was shown within the past 24 hours
|
||||
return false
|
||||
}
|
||||
|
||||
let startDate = Date(millisecondsSince1970: callSummary.startTime)
|
||||
let endDate = Date(millisecondsSince1970: callSummary.endTime)
|
||||
let callWasShort = startDate.addingTimeInterval(.minute) > endDate
|
||||
let callWasLong = startDate.addingTimeInterval(25 * .minute) < endDate
|
||||
let callLengthWasOutsideNormalRange = callWasShort || callWasLong
|
||||
|
||||
guard let localIdentifiers = deps.accountManager.localIdentifiers(tx: tx) else {
|
||||
owsFailBeta("No local identifiers", logger: logger)
|
||||
return callLengthWasOutsideNormalRange
|
||||
}
|
||||
|
||||
let odds = RemoteConfig.current.callQualitySurveyPPM(localIdentifiers: localIdentifiers)
|
||||
let passedRNGCheck = UInt32.random(in: 0..<1_000_000) < odds
|
||||
|
||||
return callLengthWasOutsideNormalRange || passedRNGCheck
|
||||
}
|
||||
|
||||
func submit(rating: CallQualitySurvey.Rating, shouldSubmitDebugLogs: Bool) {
|
||||
var proto = Proto()
|
||||
proto.callType = callType.rawValue
|
||||
setCallSummary(proto: &proto, summary: callSummary)
|
||||
setRating(proto: &proto, rating: rating)
|
||||
|
||||
Task {
|
||||
if shouldSubmitDebugLogs {
|
||||
do {
|
||||
let debugLogURL = try await DebugLogs.uploadLogs(dumper: .fromGlobals())
|
||||
proto.debugLogURL = debugLogURL.absoluteString
|
||||
} catch {
|
||||
logger.error("Failed to submit debug logs: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try proto.serializedData()
|
||||
let request = createRequest(data: data)
|
||||
let response = try await deps.networkManager.asyncRequest(
|
||||
request,
|
||||
retryPolicy: .hopefullyRecoverable,
|
||||
)
|
||||
if response.responseStatusCode != 204 {
|
||||
throw response.asError()
|
||||
}
|
||||
logger.info("Call quality survey submitted")
|
||||
} catch {
|
||||
logger.error("Failed to submit call quality survey: \(error)")
|
||||
}
|
||||
|
||||
if callSummary.isFailure {
|
||||
deps.db.write { tx in
|
||||
kvStore.writeValue(
|
||||
Date(),
|
||||
forKey: StoreKeys.lastFailureSubmittedDate,
|
||||
tx: tx,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setRating(proto: inout Proto, rating: CallQualitySurvey.Rating) {
|
||||
switch rating {
|
||||
case .satisfied:
|
||||
proto.userSatisfied = true
|
||||
proto.callQualityIssues = []
|
||||
case let .hadIssues(issues, customIssue):
|
||||
proto.userSatisfied = false
|
||||
proto.callQualityIssues = issues.map(\.rawValue)
|
||||
if let customIssue {
|
||||
proto.additionalIssuesDescription = customIssue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setCallSummary(proto: inout Proto, summary: CallSummary) {
|
||||
proto.startTimestamp = Int64(summary.startTime)
|
||||
proto.endTimestamp = Int64(summary.endTime)
|
||||
proto.callEndReason = summary.callEndReasonText
|
||||
proto.success = !summary.isFailure
|
||||
|
||||
if let value = summary.qualityStats.rttMedianConnectionMillis {
|
||||
proto.connectionRttMedian = value
|
||||
}
|
||||
if let value = summary.qualityStats.audioStats.rttMedianMillis {
|
||||
proto.audioRttMedian = value
|
||||
}
|
||||
if let value = summary.qualityStats.videoStats.rttMedianMillis {
|
||||
proto.videoRttMedian = value
|
||||
}
|
||||
if let value = summary.qualityStats.audioStats.jitterMedianReceiveMillis {
|
||||
proto.audioRecvJitterMedian = value
|
||||
}
|
||||
if let value = summary.qualityStats.videoStats.jitterMedianReceiveMillis {
|
||||
proto.videoRecvJitterMedian = value
|
||||
}
|
||||
if let value = summary.qualityStats.audioStats.jitterMedianSendMillis {
|
||||
proto.audioSendJitterMedian = value
|
||||
}
|
||||
if let value = summary.qualityStats.videoStats.jitterMedianSendMillis {
|
||||
proto.videoSendJitterMedian = value
|
||||
}
|
||||
if let value = summary.qualityStats.audioStats.packetLossFractionReceive {
|
||||
proto.audioRecvPacketLossFraction = value
|
||||
}
|
||||
if let value = summary.qualityStats.videoStats.packetLossFractionReceive {
|
||||
proto.videoRecvPacketLossFraction = value
|
||||
}
|
||||
if let value = summary.qualityStats.audioStats.packetLossFractionSend {
|
||||
proto.audioSendPacketLossFraction = value
|
||||
}
|
||||
if let value = summary.qualityStats.videoStats.packetLossFractionSend {
|
||||
proto.videoSendPacketLossFraction = value
|
||||
}
|
||||
if let value = summary.rawStats {
|
||||
proto.callTelemetry = value
|
||||
}
|
||||
}
|
||||
|
||||
private func createRequest(data: Data) -> TSRequest {
|
||||
var request = TSRequest(
|
||||
url: URL(string: "v1/call_quality_survey")!,
|
||||
method: "PUT",
|
||||
body: .data(data),
|
||||
logger: logger,
|
||||
)
|
||||
request.auth = .anonymous
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
private extension CallSummary {
|
||||
var isFailure: Bool {
|
||||
[
|
||||
"internalFailure",
|
||||
"signalingFailure",
|
||||
"connectionFailure",
|
||||
"iceFailedAfterConnected",
|
||||
].contains(callEndReasonText)
|
||||
}
|
||||
}
|
||||
@ -221,7 +221,21 @@ class GroupCall: SignalRingRTC.GroupCallDelegate {
|
||||
func groupCall(onEnded groupCall: SignalRingRTC.GroupCall, reason: CallEndReason, summary: CallSummary) {
|
||||
self.hasInvokedConnectMethod = false
|
||||
|
||||
// TODO: Handle the call summary.
|
||||
CallQualitySurveyManager(
|
||||
callSummary: summary,
|
||||
callType: {
|
||||
switch groupCall.kind {
|
||||
case .signalGroup: .group
|
||||
case .callLink: .link
|
||||
}
|
||||
}(),
|
||||
deps: .init(
|
||||
db: DependenciesBridge.shared.db,
|
||||
accountManager: DependenciesBridge.shared.tsAccountManager,
|
||||
networkManager: SSKEnvironment.shared.networkManagerRef,
|
||||
),
|
||||
).showIfNeeded()
|
||||
|
||||
observers.elements.forEach { $0.groupCallEnded(self, reason: reason) }
|
||||
}
|
||||
|
||||
|
||||
@ -458,7 +458,20 @@ final class IndividualCallService: CallServiceStateObserver {
|
||||
func callManager(_ callManager: CallService.CallManagerType, onCallEnded call: SignalCall, callId: UInt64, reason: CallEndReason, summary: CallSummary) {
|
||||
Logger.info("call: \(call), onCallEnded: \(reason)")
|
||||
|
||||
// TODO: Handle the call summary.
|
||||
CallQualitySurveyManager(
|
||||
callSummary: summary,
|
||||
callType: {
|
||||
switch call.individualCall.offerMediaType {
|
||||
case .audio: .individualAudio
|
||||
case .video: .individualVideo
|
||||
}
|
||||
}(),
|
||||
deps: .init(
|
||||
db: DependenciesBridge.shared.db,
|
||||
accountManager: tsAccountManager,
|
||||
networkManager: networkManager,
|
||||
),
|
||||
).showIfNeeded()
|
||||
|
||||
switch reason {
|
||||
case .localHangup:
|
||||
|
||||
@ -15,7 +15,19 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
|
||||
private let tableViewController = OWSTableViewController2()
|
||||
|
||||
private var shouldSubmitDebugLog = false
|
||||
private var shouldSubmitDebugLogs = false
|
||||
|
||||
private let rating: CallQualitySurvey.Rating
|
||||
|
||||
init(rating: CallQualitySurvey.Rating) {
|
||||
self.rating = rating
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
@ -63,10 +75,10 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
comment: "Label for the toggle to share debug log in the call quality survey",
|
||||
),
|
||||
isOn: { [weak self] in
|
||||
self?.shouldSubmitDebugLog ?? false
|
||||
self?.shouldSubmitDebugLogs ?? false
|
||||
},
|
||||
actionBlock: { [weak self] control in
|
||||
self?.shouldSubmitDebugLog = control.isOn
|
||||
self?.shouldSubmitDebugLogs = control.isOn
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -87,8 +99,7 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
bottomStackView.autoPinEdges(toSuperviewMarginsExcludingEdge: .top)
|
||||
|
||||
let continueButton = UIButton(primaryAction: .init { [weak self] _ in
|
||||
// [Call Quality Survey] TODO: Implement
|
||||
self?.dismiss(animated: true)
|
||||
self?.submit()
|
||||
})
|
||||
continueButton.configuration = .largePrimary(title: OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_SUBMIT_BUTTON",
|
||||
@ -114,4 +125,11 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
let bottomStackHeight = bottomStackView.height
|
||||
return headerHeight + collectionViewHeight + bottomStackHeight
|
||||
}
|
||||
|
||||
private func submit() {
|
||||
sheetNav?.submit(
|
||||
rating: self.rating,
|
||||
shouldSubmitDebugLogs: self.shouldSubmitDebugLogs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@ final class CallQualitySurveyIssuesViewController: CallQualitySurveySheetViewCon
|
||||
private let bottomStackView = UIStackView()
|
||||
private lazy var continueButton = UIButton(
|
||||
configuration: .largePrimary(title: CommonStrings.continueButton),
|
||||
primaryAction: .init { [weak sheetNav] _ in
|
||||
sheetNav?.doneSelectingIssues()
|
||||
primaryAction: .init { [weak self] _ in
|
||||
self?.submit()
|
||||
},
|
||||
)
|
||||
private lazy var customIssueEntry = UIButton(
|
||||
@ -36,7 +36,7 @@ final class CallQualitySurveyIssuesViewController: CallQualitySurveySheetViewCon
|
||||
return UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
}()
|
||||
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>?
|
||||
private var dataSource: DataSource?
|
||||
|
||||
private var selectedItems = Set<Item>()
|
||||
private var customIssue: String? {
|
||||
@ -115,11 +115,10 @@ final class CallQualitySurveyIssuesViewController: CallQualitySurveySheetViewCon
|
||||
}
|
||||
|
||||
let cellRegistration = UICollectionView.CellRegistration<CapsuleCell, Item> { cell, _, item in
|
||||
// TODO: Account for selected icons
|
||||
cell.configure(title: item.title, image: item.image)
|
||||
}
|
||||
|
||||
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||
dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||
collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
|
||||
}
|
||||
|
||||
@ -135,15 +134,14 @@ final class CallQualitySurveyIssuesViewController: CallQualitySurveySheetViewCon
|
||||
}
|
||||
|
||||
private func loadInitialSnapshot() {
|
||||
// TODO: Typealiases?
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
var snapshot = Snapshot()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems([.audio, .video, .callDropped, .other])
|
||||
dataSource?.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
private func updateSnapshot() {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
var snapshot = Snapshot()
|
||||
snapshot.appendSections([.main])
|
||||
|
||||
// Audio
|
||||
@ -233,98 +231,97 @@ final class CallQualitySurveyIssuesViewController: CallQualitySurveySheetViewCon
|
||||
present(OWSNavigationController(rootViewController: vc), animated: true)
|
||||
}
|
||||
|
||||
private enum Item: Hashable {
|
||||
case audio
|
||||
case audioStuttering
|
||||
case audioLocalEcho
|
||||
case audioRemoteEcho
|
||||
case audioDrop
|
||||
case video
|
||||
case videoNoCamera
|
||||
case videoLowQuality
|
||||
case videoLowResolution
|
||||
case callDropped
|
||||
case other
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .audio:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_AUDIO",
|
||||
comment: "Label for audio issue option in call quality survey",
|
||||
)
|
||||
case .audioStuttering:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_AUDIO_STUTTERING",
|
||||
comment: "Label for audio stuttering issue option in call quality survey",
|
||||
)
|
||||
case .audioLocalEcho:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_AUDIO_LOCAL_ECHO",
|
||||
comment: "Label for local echo issue option in call quality survey, indicating the user heard an echo",
|
||||
)
|
||||
case .audioRemoteEcho:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_AUDIO_REMOTE_ECHO",
|
||||
comment: "Label for remote echo issue option in call quality survey, indicating other participants heard an echo",
|
||||
)
|
||||
case .audioDrop:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_AUDIO_DROP",
|
||||
comment: "Label for audio dropout issue option in call quality survey",
|
||||
)
|
||||
case .video:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_VIDEO",
|
||||
comment: "Label for video issue option in call quality survey",
|
||||
)
|
||||
case .videoNoCamera:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_VIDEO_NO_CAMERA",
|
||||
comment: "Label for camera not working issue option in call quality survey",
|
||||
)
|
||||
case .videoLowQuality:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_VIDEO_LOW_QUALITY",
|
||||
comment: "Label for poor video quality issue option in call quality survey",
|
||||
)
|
||||
case .videoLowResolution:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_VIDEO_LOW_RESOLUTION",
|
||||
comment: "Label for low resolution video issue option in call quality survey",
|
||||
)
|
||||
case .callDropped:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_CALL_DROPPED",
|
||||
comment: "Label for call dropped issue option in call quality survey",
|
||||
)
|
||||
case .other:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_OTHER",
|
||||
comment: "Label for custom issue option in call quality survey",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var image: ImageResource {
|
||||
switch self {
|
||||
case .audio, .audioStuttering, .audioLocalEcho, .audioRemoteEcho, .audioDrop:
|
||||
.speaker
|
||||
case .video, .videoNoCamera, .videoLowQuality, .videoLowResolution:
|
||||
.video
|
||||
case .callDropped:
|
||||
.xCircle
|
||||
case .other:
|
||||
.errorCircle
|
||||
}
|
||||
}
|
||||
private func submit() {
|
||||
sheetNav?.doneSelectingIssues(
|
||||
rating: .hadIssues(selectedItems, customIssue: customIssue),
|
||||
)
|
||||
}
|
||||
|
||||
private typealias DataSource = UICollectionViewDiffableDataSource<Section, Item>
|
||||
private typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>
|
||||
|
||||
private typealias Item = CallQualitySurvey.Issue
|
||||
|
||||
private enum Section: Hashable {
|
||||
case main
|
||||
}
|
||||
}
|
||||
|
||||
private extension CallQualitySurvey.Issue {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .audio:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_AUDIO",
|
||||
comment: "Label for audio issue option in call quality survey",
|
||||
)
|
||||
case .audioStuttering:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_AUDIO_STUTTERING",
|
||||
comment: "Label for audio stuttering issue option in call quality survey",
|
||||
)
|
||||
case .audioLocalEcho:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_AUDIO_LOCAL_ECHO",
|
||||
comment: "Label for local echo issue option in call quality survey, indicating the user heard an echo",
|
||||
)
|
||||
case .audioRemoteEcho:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_AUDIO_REMOTE_ECHO",
|
||||
comment: "Label for remote echo issue option in call quality survey, indicating other participants heard an echo",
|
||||
)
|
||||
case .audioDrop:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_AUDIO_DROP",
|
||||
comment: "Label for audio dropout issue option in call quality survey",
|
||||
)
|
||||
case .video:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_VIDEO",
|
||||
comment: "Label for video issue option in call quality survey",
|
||||
)
|
||||
case .videoNoCamera:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_VIDEO_NO_CAMERA",
|
||||
comment: "Label for camera not working issue option in call quality survey",
|
||||
)
|
||||
case .videoLowQuality:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_VIDEO_LOW_QUALITY",
|
||||
comment: "Label for poor video quality issue option in call quality survey",
|
||||
)
|
||||
case .videoLowResolution:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_VIDEO_LOW_RESOLUTION",
|
||||
comment: "Label for low resolution video issue option in call quality survey",
|
||||
)
|
||||
case .callDropped:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_CALL_DROPPED",
|
||||
comment: "Label for call dropped issue option in call quality survey",
|
||||
)
|
||||
case .other:
|
||||
OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_ISSUE_OTHER",
|
||||
comment: "Label for custom issue option in call quality survey",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var image: ImageResource {
|
||||
switch self {
|
||||
case .audio, .audioStuttering, .audioLocalEcho, .audioRemoteEcho, .audioDrop:
|
||||
.speaker
|
||||
case .video, .videoNoCamera, .videoLowQuality, .videoLowResolution:
|
||||
.video
|
||||
case .callDropped:
|
||||
.xCircle
|
||||
case .other:
|
||||
.errorCircle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CallQualitySurveyCustomIssueViewController.Delegate
|
||||
|
||||
extension CallQualitySurveyIssuesViewController: CallQualitySurveyCustomIssueViewController.Delegate {
|
||||
|
||||
@ -9,7 +9,10 @@ import SignalUI
|
||||
// MARK: - CallQualitySurveyNavigationController
|
||||
|
||||
final class CallQualitySurveyNavigationController: UINavigationController {
|
||||
init() {
|
||||
private let callQualitySurveyManager: CallQualitySurveyManager
|
||||
|
||||
init(callQualitySurveyManager: CallQualitySurveyManager) {
|
||||
self.callQualitySurveyManager = callQualitySurveyManager
|
||||
let vc = CallQualitySurveyRatingViewController()
|
||||
super.init(rootViewController: vc)
|
||||
vc.navigationItem.rightBarButtonItem = .cancelButton(dismissingFrom: self)
|
||||
@ -71,15 +74,28 @@ final class CallQualitySurveyNavigationController: UINavigationController {
|
||||
pushViewController(vc, animated: false)
|
||||
}
|
||||
|
||||
// [Call Quality Survey] TODO: Pass state through
|
||||
func doneSelectingIssues() {
|
||||
let vc = SurveyDebugLogViewController()
|
||||
func doneSelectingIssues(rating: CallQualitySurvey.Rating) {
|
||||
let vc = SurveyDebugLogViewController(rating: rating)
|
||||
vc.navigationItem.rightBarButtonItem = .cancelButton(dismissingFrom: self)
|
||||
vc.navigationItem.leftBarButtonItem = makeBackButton()
|
||||
addFadeTransition()
|
||||
pushViewController(vc, animated: false)
|
||||
}
|
||||
|
||||
func submit(rating: CallQualitySurvey.Rating, shouldSubmitDebugLogs: Bool) {
|
||||
callQualitySurveyManager.submit(
|
||||
rating: rating,
|
||||
shouldSubmitDebugLogs: shouldSubmitDebugLogs,
|
||||
)
|
||||
let host = presentingViewController
|
||||
dismiss(animated: true) {
|
||||
host?.presentToast(text: OWSLocalizedString(
|
||||
"CALL_QUALITY_SURVEY_COMPLETION_TOAST",
|
||||
comment: "Title for toast which appears after submitting a call quality survey",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func makeBackButton() -> UIBarButtonItem {
|
||||
UIBarButtonItem.button(
|
||||
image: UIImage(resource: .chevronLeftBold28),
|
||||
|
||||
@ -58,8 +58,7 @@ final class CallQualitySurveyRatingViewController: CallQualitySurveySheetViewCon
|
||||
comment: "Button label for indicating the call did not have issues in the call quality survey",
|
||||
),
|
||||
) { [weak sheetNav] in
|
||||
// [Call Quality Survey] TODO: Pass selected items
|
||||
sheetNav?.doneSelectingIssues()
|
||||
sheetNav?.doneSelectingIssues(rating: .satisfied)
|
||||
}
|
||||
|
||||
// Zero-width spacer views with .equalSpacing distribution makes the
|
||||
|
||||
@ -327,10 +327,17 @@ class InternalSettingsViewController: OWSTableViewController2 {
|
||||
otherSection.add(.copyableItem(label: "Audio Category", value: AVAudioSession.sharedInstance().category.rawValue.replacingOccurrences(of: "AVAudioSessionCategory", with: "")))
|
||||
otherSection.add(.switch(
|
||||
withText: "Spinning checkmarks",
|
||||
isOn: { SpinningCheckmarks.shouldSpin },
|
||||
isOn: { InMemorySettings.spinningCheckmarks },
|
||||
target: self,
|
||||
selector: #selector(spinCheckmarks(_:)),
|
||||
))
|
||||
otherSection.add(.switch(
|
||||
withText: "Force call quality survey",
|
||||
isOn: { InMemorySettings.forceCallQualitySurvey },
|
||||
actionBlock: { _ in
|
||||
InMemorySettings.forceCallQualitySurvey.toggle()
|
||||
},
|
||||
))
|
||||
if #available(iOS 26, *) {
|
||||
otherSection.add(.switch(
|
||||
withText: "Disable Content Tracking in Chat Header",
|
||||
@ -339,9 +346,6 @@ class InternalSettingsViewController: OWSTableViewController2 {
|
||||
selector: #selector(toggleChatHeaderContentTrackingDisabled(sender:)),
|
||||
))
|
||||
}
|
||||
otherSection.add(.disclosureItem(withText: "Show Call Quality Survey") { [weak self] in
|
||||
self?.present(CallQualitySurveyNavigationController(), animated: true)
|
||||
})
|
||||
contents.add(otherSection)
|
||||
|
||||
if mode != .registration {
|
||||
@ -366,6 +370,11 @@ class InternalSettingsViewController: OWSTableViewController2 {
|
||||
|
||||
// MARK: -
|
||||
|
||||
public enum InMemorySettings {
|
||||
static var spinningCheckmarks = false
|
||||
static var forceCallQualitySurvey = false
|
||||
}
|
||||
|
||||
private extension InternalSettingsViewController {
|
||||
|
||||
var isChatHeaderContentTrackingDisabled: Bool {
|
||||
@ -380,17 +389,11 @@ private extension InternalSettingsViewController {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
public enum SpinningCheckmarks {
|
||||
static var shouldSpin = false
|
||||
}
|
||||
|
||||
private extension InternalSettingsViewController {
|
||||
|
||||
@objc
|
||||
func spinCheckmarks(_ sender: Any) {
|
||||
let wasSpinning = SpinningCheckmarks.shouldSpin
|
||||
let wasSpinning = InMemorySettings.spinningCheckmarks
|
||||
if let view = sender as? UIView {
|
||||
if wasSpinning {
|
||||
view.layer.removeAnimation(forKey: "spin")
|
||||
@ -403,7 +406,7 @@ private extension InternalSettingsViewController {
|
||||
view.layer.add(animation, forKey: "spin")
|
||||
}
|
||||
}
|
||||
SpinningCheckmarks.shouldSpin = !wasSpinning
|
||||
InMemorySettings.spinningCheckmarks = !wasSpinning
|
||||
}
|
||||
|
||||
func exportMessageBackupProto() async {
|
||||
|
||||
@ -673,7 +673,7 @@ class ChatListCell: UITableViewCell, ReusableTableViewCell {
|
||||
messageStatusIconView.image = token.image.withRenderingMode(.alwaysTemplate)
|
||||
messageStatusIconView.tintColor = token.tintColor
|
||||
|
||||
if token.shouldAnimateStatusIcon || SpinningCheckmarks.shouldSpin {
|
||||
if token.shouldAnimateStatusIcon || InMemorySettings.spinningCheckmarks {
|
||||
messageStatusIconView.startSpinning()
|
||||
} else {
|
||||
messageStatusIconView.stopSpinning()
|
||||
|
||||
@ -1381,6 +1381,9 @@
|
||||
/* notification body */
|
||||
"CALL_MISSED_BECAUSE_OF_IDENTITY_CHANGE_NOTIFICATION_BODY" = "☎️ Missed call because the caller's safety number changed.";
|
||||
|
||||
/* Title for toast which appears after submitting a call quality survey */
|
||||
"CALL_QUALITY_SURVEY_COMPLETION_TOAST" = "Thanks for your feedback!";
|
||||
|
||||
/* Footer text explaining custom issue descriptions in the call quality survey */
|
||||
"CALL_QUALITY_SURVEY_CUSTOM_ISSUE_FOOTER" = "Please include any details relevant to the issue. Anything you share here will be kept private and will only be used to help improve calls in Signal.";
|
||||
|
||||
|
||||
@ -9,14 +9,15 @@ import Foundation
|
||||
@inlinable
|
||||
public func owsFailBeta(
|
||||
_ logMessage: String,
|
||||
logger: PrefixedLogger = .empty(),
|
||||
file: String = #fileID,
|
||||
function: String = #function,
|
||||
line: Int = #line,
|
||||
) {
|
||||
if BuildFlags.isPrerelease {
|
||||
owsFail(logMessage, file: file, function: function, line: line)
|
||||
owsFail(logMessage, logger: logger, file: file, function: function, line: line)
|
||||
} else {
|
||||
owsFailDebug(logMessage, file: file, function: function, line: line)
|
||||
owsFailDebug(logMessage, logger: logger, file: file, function: function, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -253,6 +253,20 @@ public class RemoteConfig {
|
||||
return TimeInterval(intervalMs) / 1000
|
||||
}
|
||||
|
||||
/// How many successful calls per million should show a call quality survey for the user's region
|
||||
public func callQualitySurveyPPM(localIdentifiers: LocalIdentifiers) -> UInt64 {
|
||||
let defaultValue: UInt64 = 10_000
|
||||
let string = Self.countryCodeBucketValue(
|
||||
csvString: getStringConvertibleValue(
|
||||
forFlag: .callQualitySurveyPPM,
|
||||
defaultValue: "*:\(defaultValue)",
|
||||
),
|
||||
localIdentifiers: localIdentifiers,
|
||||
)
|
||||
guard let string else { return defaultValue }
|
||||
return UInt64(string) ?? defaultValue
|
||||
}
|
||||
|
||||
public var mediaTierFallbackCdnNumber: UInt32 {
|
||||
getUInt32Value(forFlag: .mediaTierFallbackCdnNumber, defaultValue: 3)
|
||||
}
|
||||
@ -386,16 +400,23 @@ public class RemoteConfig {
|
||||
|
||||
// MARK: - Country code buckets
|
||||
|
||||
private static func countryCodeBucketValue(csvString: String, localIdentifiers: LocalIdentifiers) -> String? {
|
||||
let phoneNumberUtil = SSKEnvironment.shared.phoneNumberUtilRef
|
||||
let callingCode = phoneNumberUtil.parseE164(localIdentifiers.phoneNumber)?.getCallingCode()
|
||||
return countryCodeValue(csvString: csvString, callingCode: callingCode)
|
||||
}
|
||||
|
||||
/// Determine if a country-code-dependent flag is enabled for the current
|
||||
/// user, given a country-code CSV and key.
|
||||
///
|
||||
/// - Parameter csvString: a CSV containing `<country-code>:<parts-per-million>` pairs
|
||||
/// - Parameter key: a key to use as part of bucketing
|
||||
static func isCountryCodeBucketEnabled(csvString: String, key: String, localIdentifiers: LocalIdentifiers) -> Bool {
|
||||
let phoneNumberUtil = SSKEnvironment.shared.phoneNumberUtilRef
|
||||
let callingCode = phoneNumberUtil.parseE164(localIdentifiers.phoneNumber)?.getCallingCode()
|
||||
guard
|
||||
let countryCodeValue = countryCodeValue(csvString: csvString, callingCode: callingCode),
|
||||
let countryCodeValue = countryCodeBucketValue(
|
||||
csvString: csvString,
|
||||
localIdentifiers: localIdentifiers,
|
||||
),
|
||||
let countEnabled = UInt64(countryCodeValue)
|
||||
else {
|
||||
return false
|
||||
@ -557,6 +578,7 @@ private enum ValueFlag: String, FlagType {
|
||||
case attachmentMaxEncryptedBytes = "global.attachments.maxBytes"
|
||||
case automaticSessionResetAttemptInterval = "ios.automaticSessionResetAttemptInterval"
|
||||
case backgroundRefreshInterval = "ios.backgroundRefreshInterval"
|
||||
case callQualitySurveyPPM = "ios.callQualitySurveyPPM"
|
||||
case cdsSyncInterval = "cds.syncInterval.seconds"
|
||||
case clientExpiration = "ios.clientExpiration"
|
||||
case creditAndDebitCardDisabledRegions = "global.donations.ccDisabledRegions"
|
||||
@ -591,6 +613,7 @@ private enum ValueFlag: String, FlagType {
|
||||
case .attachmentMaxEncryptedBytes: false
|
||||
case .automaticSessionResetAttemptInterval: true
|
||||
case .backgroundRefreshInterval: true
|
||||
case .callQualitySurveyPPM: true
|
||||
case .cdsSyncInterval: false
|
||||
case .clientExpiration: true
|
||||
case .creditAndDebitCardDisabledRegions: true
|
||||
|
||||
463
SignalServiceKit/Protos/Generated/CallQualitySurvey.pb.swift
Normal file
463
SignalServiceKit/Protos/Generated/CallQualitySurvey.pb.swift
Normal file
@ -0,0 +1,463 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// DO NOT EDIT.
|
||||
// swift-format-ignore-file
|
||||
// swiftlint:disable all
|
||||
//
|
||||
// Generated by the Swift generator plugin for the protocol buffer compiler.
|
||||
// Source: CallQualitySurvey.proto
|
||||
//
|
||||
// For information on using the generated types, please see the documentation:
|
||||
// https://github.com/apple/swift-protobuf/
|
||||
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
public import Foundation
|
||||
public import SwiftProtobuf
|
||||
|
||||
// If the compiler emits an error on this type, it is because this file
|
||||
// was generated by a version of the `protoc` Swift plug-in that is
|
||||
// incompatible with the version of SwiftProtobuf to which you are linking.
|
||||
// Please ensure that you are building against the same version of the API
|
||||
// that was used to generate this file.
|
||||
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
|
||||
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
|
||||
typealias Version = _2
|
||||
}
|
||||
|
||||
public struct CallQualitySurveyProtos_SubmitCallQualitySurveyRequest: @unchecked Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
/// Indicates whether the caller was generally satisfied with the quality of
|
||||
/// the call
|
||||
public var userSatisfied: Bool {
|
||||
get {return _storage._userSatisfied}
|
||||
set {_uniqueStorage()._userSatisfied = newValue}
|
||||
}
|
||||
|
||||
/// A list of call quality issues selected by the caller
|
||||
public var callQualityIssues: [String] {
|
||||
get {return _storage._callQualityIssues}
|
||||
set {_uniqueStorage()._callQualityIssues = newValue}
|
||||
}
|
||||
|
||||
/// A free-form description of any additional issues as written by the caller
|
||||
public var additionalIssuesDescription: String {
|
||||
get {return _storage._additionalIssuesDescription ?? String()}
|
||||
set {_uniqueStorage()._additionalIssuesDescription = newValue}
|
||||
}
|
||||
/// Returns true if `additionalIssuesDescription` has been explicitly set.
|
||||
public var hasAdditionalIssuesDescription: Bool {return _storage._additionalIssuesDescription != nil}
|
||||
/// Clears the value of `additionalIssuesDescription`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearAdditionalIssuesDescription() {_uniqueStorage()._additionalIssuesDescription = nil}
|
||||
|
||||
/// A URL for a set of debug logs associated with the call if the caller chose
|
||||
/// to submit debug logs
|
||||
public var debugLogURL: String {
|
||||
get {return _storage._debugLogURL ?? String()}
|
||||
set {_uniqueStorage()._debugLogURL = newValue}
|
||||
}
|
||||
/// Returns true if `debugLogURL` has been explicitly set.
|
||||
public var hasDebugLogURL: Bool {return _storage._debugLogURL != nil}
|
||||
/// Clears the value of `debugLogURL`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearDebugLogURL() {_uniqueStorage()._debugLogURL = nil}
|
||||
|
||||
/// The time at which the call started in milliseconds since the epoch
|
||||
public var startTimestamp: Int64 {
|
||||
get {return _storage._startTimestamp}
|
||||
set {_uniqueStorage()._startTimestamp = newValue}
|
||||
}
|
||||
|
||||
/// The time at which the call ended in milliseconds since the epoch
|
||||
public var endTimestamp: Int64 {
|
||||
get {return _storage._endTimestamp}
|
||||
set {_uniqueStorage()._endTimestamp = newValue}
|
||||
}
|
||||
|
||||
/// The type of call; note that direct voice calls can become video calls and
|
||||
/// vice versa, and this field indicates which mode was selected at call
|
||||
/// initiation time. At the time of writing, expected call types are
|
||||
/// "direct_voice", "direct_video", "group", and "call_link".
|
||||
public var callType: String {
|
||||
get {return _storage._callType}
|
||||
set {_uniqueStorage()._callType = newValue}
|
||||
}
|
||||
|
||||
/// Indicates whether the call completed without error or if it terminated
|
||||
/// abnormally
|
||||
public var success: Bool {
|
||||
get {return _storage._success}
|
||||
set {_uniqueStorage()._success = newValue}
|
||||
}
|
||||
|
||||
/// A client-defined, but human-readable reason for call termination
|
||||
public var callEndReason: String {
|
||||
get {return _storage._callEndReason}
|
||||
set {_uniqueStorage()._callEndReason = newValue}
|
||||
}
|
||||
|
||||
/// The median round-trip time, measured in milliseconds, for STUN/ICE packets
|
||||
/// (i.e. connection maintenance and establishment)
|
||||
public var connectionRttMedian: Float {
|
||||
get {return _storage._connectionRttMedian ?? 0}
|
||||
set {_uniqueStorage()._connectionRttMedian = newValue}
|
||||
}
|
||||
/// Returns true if `connectionRttMedian` has been explicitly set.
|
||||
public var hasConnectionRttMedian: Bool {return _storage._connectionRttMedian != nil}
|
||||
/// Clears the value of `connectionRttMedian`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearConnectionRttMedian() {_uniqueStorage()._connectionRttMedian = nil}
|
||||
|
||||
/// The median round-trip time, measured in milliseconds, for RTP/RTCP packets
|
||||
/// for audio streams
|
||||
public var audioRttMedian: Float {
|
||||
get {return _storage._audioRttMedian ?? 0}
|
||||
set {_uniqueStorage()._audioRttMedian = newValue}
|
||||
}
|
||||
/// Returns true if `audioRttMedian` has been explicitly set.
|
||||
public var hasAudioRttMedian: Bool {return _storage._audioRttMedian != nil}
|
||||
/// Clears the value of `audioRttMedian`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearAudioRttMedian() {_uniqueStorage()._audioRttMedian = nil}
|
||||
|
||||
/// The median round-trip time, measured in milliseconds, for RTP/RTCP packets
|
||||
/// for video streams
|
||||
public var videoRttMedian: Float {
|
||||
get {return _storage._videoRttMedian ?? 0}
|
||||
set {_uniqueStorage()._videoRttMedian = newValue}
|
||||
}
|
||||
/// Returns true if `videoRttMedian` has been explicitly set.
|
||||
public var hasVideoRttMedian: Bool {return _storage._videoRttMedian != nil}
|
||||
/// Clears the value of `videoRttMedian`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearVideoRttMedian() {_uniqueStorage()._videoRttMedian = nil}
|
||||
|
||||
/// The median jitter for audio streams, measured in milliseconds, for the
|
||||
/// duration of the call as measured by the client submitting the survey
|
||||
public var audioRecvJitterMedian: Float {
|
||||
get {return _storage._audioRecvJitterMedian ?? 0}
|
||||
set {_uniqueStorage()._audioRecvJitterMedian = newValue}
|
||||
}
|
||||
/// Returns true if `audioRecvJitterMedian` has been explicitly set.
|
||||
public var hasAudioRecvJitterMedian: Bool {return _storage._audioRecvJitterMedian != nil}
|
||||
/// Clears the value of `audioRecvJitterMedian`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearAudioRecvJitterMedian() {_uniqueStorage()._audioRecvJitterMedian = nil}
|
||||
|
||||
/// The median jitter for video streams, measured in milliseconds, for the
|
||||
/// duration of the call as measured by the client submitting the survey
|
||||
public var videoRecvJitterMedian: Float {
|
||||
get {return _storage._videoRecvJitterMedian ?? 0}
|
||||
set {_uniqueStorage()._videoRecvJitterMedian = newValue}
|
||||
}
|
||||
/// Returns true if `videoRecvJitterMedian` has been explicitly set.
|
||||
public var hasVideoRecvJitterMedian: Bool {return _storage._videoRecvJitterMedian != nil}
|
||||
/// Clears the value of `videoRecvJitterMedian`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearVideoRecvJitterMedian() {_uniqueStorage()._videoRecvJitterMedian = nil}
|
||||
|
||||
/// The median jitter for audio streams, measured in milliseconds, for the
|
||||
/// duration of the call as measured by the remote endpoint in the call (either
|
||||
/// the peer of the client submitting the survey in a direct call or the SFU in
|
||||
/// a group call)
|
||||
public var audioSendJitterMedian: Float {
|
||||
get {return _storage._audioSendJitterMedian ?? 0}
|
||||
set {_uniqueStorage()._audioSendJitterMedian = newValue}
|
||||
}
|
||||
/// Returns true if `audioSendJitterMedian` has been explicitly set.
|
||||
public var hasAudioSendJitterMedian: Bool {return _storage._audioSendJitterMedian != nil}
|
||||
/// Clears the value of `audioSendJitterMedian`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearAudioSendJitterMedian() {_uniqueStorage()._audioSendJitterMedian = nil}
|
||||
|
||||
/// The median jitter for video streams, measured in milliseconds, for the
|
||||
/// duration of the call as measured by the remote endpoint in the call (either
|
||||
/// the peer of the client submitting the survey in a direct call or the SFU in
|
||||
/// a group call)
|
||||
public var videoSendJitterMedian: Float {
|
||||
get {return _storage._videoSendJitterMedian ?? 0}
|
||||
set {_uniqueStorage()._videoSendJitterMedian = newValue}
|
||||
}
|
||||
/// Returns true if `videoSendJitterMedian` has been explicitly set.
|
||||
public var hasVideoSendJitterMedian: Bool {return _storage._videoSendJitterMedian != nil}
|
||||
/// Clears the value of `videoSendJitterMedian`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearVideoSendJitterMedian() {_uniqueStorage()._videoSendJitterMedian = nil}
|
||||
|
||||
/// The fraction of audio packets lost over the duration of the call as
|
||||
/// measured by the client submitting the survey
|
||||
public var audioRecvPacketLossFraction: Float {
|
||||
get {return _storage._audioRecvPacketLossFraction ?? 0}
|
||||
set {_uniqueStorage()._audioRecvPacketLossFraction = newValue}
|
||||
}
|
||||
/// Returns true if `audioRecvPacketLossFraction` has been explicitly set.
|
||||
public var hasAudioRecvPacketLossFraction: Bool {return _storage._audioRecvPacketLossFraction != nil}
|
||||
/// Clears the value of `audioRecvPacketLossFraction`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearAudioRecvPacketLossFraction() {_uniqueStorage()._audioRecvPacketLossFraction = nil}
|
||||
|
||||
/// The fraction of video packets lost over the duration of the call as
|
||||
/// measured by the client submitting the survey
|
||||
public var videoRecvPacketLossFraction: Float {
|
||||
get {return _storage._videoRecvPacketLossFraction ?? 0}
|
||||
set {_uniqueStorage()._videoRecvPacketLossFraction = newValue}
|
||||
}
|
||||
/// Returns true if `videoRecvPacketLossFraction` has been explicitly set.
|
||||
public var hasVideoRecvPacketLossFraction: Bool {return _storage._videoRecvPacketLossFraction != nil}
|
||||
/// Clears the value of `videoRecvPacketLossFraction`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearVideoRecvPacketLossFraction() {_uniqueStorage()._videoRecvPacketLossFraction = nil}
|
||||
|
||||
/// The fraction of audio packets lost over the duration of the call as
|
||||
/// measured by the remote endpoint in the call (either the peer of the client
|
||||
/// submitting the survey in a direct call or the SFU in a group call)
|
||||
public var audioSendPacketLossFraction: Float {
|
||||
get {return _storage._audioSendPacketLossFraction ?? 0}
|
||||
set {_uniqueStorage()._audioSendPacketLossFraction = newValue}
|
||||
}
|
||||
/// Returns true if `audioSendPacketLossFraction` has been explicitly set.
|
||||
public var hasAudioSendPacketLossFraction: Bool {return _storage._audioSendPacketLossFraction != nil}
|
||||
/// Clears the value of `audioSendPacketLossFraction`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearAudioSendPacketLossFraction() {_uniqueStorage()._audioSendPacketLossFraction = nil}
|
||||
|
||||
/// The fraction of video packets lost over the duration of the call as
|
||||
/// measured by the remote endpoint in the call (either the peer of the client
|
||||
/// submitting the survey in a direct call or the SFU in a group call)
|
||||
public var videoSendPacketLossFraction: Float {
|
||||
get {return _storage._videoSendPacketLossFraction ?? 0}
|
||||
set {_uniqueStorage()._videoSendPacketLossFraction = newValue}
|
||||
}
|
||||
/// Returns true if `videoSendPacketLossFraction` has been explicitly set.
|
||||
public var hasVideoSendPacketLossFraction: Bool {return _storage._videoSendPacketLossFraction != nil}
|
||||
/// Clears the value of `videoSendPacketLossFraction`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearVideoSendPacketLossFraction() {_uniqueStorage()._videoSendPacketLossFraction = nil}
|
||||
|
||||
/// Machine-generated telemetry from the call; this is a serialized protobuf
|
||||
/// entity generated (and, critically, explained to the user!) by the calling
|
||||
/// library
|
||||
public var callTelemetry: Data {
|
||||
get {return _storage._callTelemetry ?? Data()}
|
||||
set {_uniqueStorage()._callTelemetry = newValue}
|
||||
}
|
||||
/// Returns true if `callTelemetry` has been explicitly set.
|
||||
public var hasCallTelemetry: Bool {return _storage._callTelemetry != nil}
|
||||
/// Clears the value of `callTelemetry`. Subsequent reads from it will return its default value.
|
||||
public mutating func clearCallTelemetry() {_uniqueStorage()._callTelemetry = nil}
|
||||
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
public init() {}
|
||||
|
||||
fileprivate var _storage = _StorageClass.defaultInstance
|
||||
}
|
||||
|
||||
// MARK: - Code below here is support for the SwiftProtobuf runtime.
|
||||
|
||||
fileprivate let _protobuf_package = "CallQualitySurveyProtos"
|
||||
|
||||
extension CallQualitySurveyProtos_SubmitCallQualitySurveyRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
public static let protoMessageName: String = _protobuf_package + ".SubmitCallQualitySurveyRequest"
|
||||
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_satisfied\0\u{3}call_quality_issues\0\u{3}additional_issues_description\0\u{3}debug_log_url\0\u{3}start_timestamp\0\u{3}end_timestamp\0\u{3}call_type\0\u{1}success\0\u{3}call_end_reason\0\u{3}connection_rtt_median\0\u{3}audio_rtt_median\0\u{3}video_rtt_median\0\u{3}audio_recv_jitter_median\0\u{3}video_recv_jitter_median\0\u{3}audio_send_jitter_median\0\u{3}video_send_jitter_median\0\u{3}audio_recv_packet_loss_fraction\0\u{3}video_recv_packet_loss_fraction\0\u{3}audio_send_packet_loss_fraction\0\u{3}video_send_packet_loss_fraction\0\u{3}call_telemetry\0")
|
||||
|
||||
fileprivate class _StorageClass {
|
||||
var _userSatisfied: Bool = false
|
||||
var _callQualityIssues: [String] = []
|
||||
var _additionalIssuesDescription: String? = nil
|
||||
var _debugLogURL: String? = nil
|
||||
var _startTimestamp: Int64 = 0
|
||||
var _endTimestamp: Int64 = 0
|
||||
var _callType: String = String()
|
||||
var _success: Bool = false
|
||||
var _callEndReason: String = String()
|
||||
var _connectionRttMedian: Float? = nil
|
||||
var _audioRttMedian: Float? = nil
|
||||
var _videoRttMedian: Float? = nil
|
||||
var _audioRecvJitterMedian: Float? = nil
|
||||
var _videoRecvJitterMedian: Float? = nil
|
||||
var _audioSendJitterMedian: Float? = nil
|
||||
var _videoSendJitterMedian: Float? = nil
|
||||
var _audioRecvPacketLossFraction: Float? = nil
|
||||
var _videoRecvPacketLossFraction: Float? = nil
|
||||
var _audioSendPacketLossFraction: Float? = nil
|
||||
var _videoSendPacketLossFraction: Float? = nil
|
||||
var _callTelemetry: Data? = nil
|
||||
|
||||
// This property is used as the initial default value for new instances of the type.
|
||||
// The type itself is protecting the reference to its storage via CoW semantics.
|
||||
// This will force a copy to be made of this reference when the first mutation occurs;
|
||||
// hence, it is safe to mark this as `nonisolated(unsafe)`.
|
||||
static nonisolated(unsafe) let defaultInstance = _StorageClass()
|
||||
|
||||
private init() {}
|
||||
|
||||
init(copying source: _StorageClass) {
|
||||
_userSatisfied = source._userSatisfied
|
||||
_callQualityIssues = source._callQualityIssues
|
||||
_additionalIssuesDescription = source._additionalIssuesDescription
|
||||
_debugLogURL = source._debugLogURL
|
||||
_startTimestamp = source._startTimestamp
|
||||
_endTimestamp = source._endTimestamp
|
||||
_callType = source._callType
|
||||
_success = source._success
|
||||
_callEndReason = source._callEndReason
|
||||
_connectionRttMedian = source._connectionRttMedian
|
||||
_audioRttMedian = source._audioRttMedian
|
||||
_videoRttMedian = source._videoRttMedian
|
||||
_audioRecvJitterMedian = source._audioRecvJitterMedian
|
||||
_videoRecvJitterMedian = source._videoRecvJitterMedian
|
||||
_audioSendJitterMedian = source._audioSendJitterMedian
|
||||
_videoSendJitterMedian = source._videoSendJitterMedian
|
||||
_audioRecvPacketLossFraction = source._audioRecvPacketLossFraction
|
||||
_videoRecvPacketLossFraction = source._videoRecvPacketLossFraction
|
||||
_audioSendPacketLossFraction = source._audioSendPacketLossFraction
|
||||
_videoSendPacketLossFraction = source._videoSendPacketLossFraction
|
||||
_callTelemetry = source._callTelemetry
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate mutating func _uniqueStorage() -> _StorageClass {
|
||||
if !isKnownUniquelyReferenced(&_storage) {
|
||||
_storage = _StorageClass(copying: _storage)
|
||||
}
|
||||
return _storage
|
||||
}
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
_ = _uniqueStorage()
|
||||
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularBoolField(value: &_storage._userSatisfied) }()
|
||||
case 2: try { try decoder.decodeRepeatedStringField(value: &_storage._callQualityIssues) }()
|
||||
case 3: try { try decoder.decodeSingularStringField(value: &_storage._additionalIssuesDescription) }()
|
||||
case 4: try { try decoder.decodeSingularStringField(value: &_storage._debugLogURL) }()
|
||||
case 5: try { try decoder.decodeSingularInt64Field(value: &_storage._startTimestamp) }()
|
||||
case 6: try { try decoder.decodeSingularInt64Field(value: &_storage._endTimestamp) }()
|
||||
case 7: try { try decoder.decodeSingularStringField(value: &_storage._callType) }()
|
||||
case 8: try { try decoder.decodeSingularBoolField(value: &_storage._success) }()
|
||||
case 9: try { try decoder.decodeSingularStringField(value: &_storage._callEndReason) }()
|
||||
case 10: try { try decoder.decodeSingularFloatField(value: &_storage._connectionRttMedian) }()
|
||||
case 11: try { try decoder.decodeSingularFloatField(value: &_storage._audioRttMedian) }()
|
||||
case 12: try { try decoder.decodeSingularFloatField(value: &_storage._videoRttMedian) }()
|
||||
case 13: try { try decoder.decodeSingularFloatField(value: &_storage._audioRecvJitterMedian) }()
|
||||
case 14: try { try decoder.decodeSingularFloatField(value: &_storage._videoRecvJitterMedian) }()
|
||||
case 15: try { try decoder.decodeSingularFloatField(value: &_storage._audioSendJitterMedian) }()
|
||||
case 16: try { try decoder.decodeSingularFloatField(value: &_storage._videoSendJitterMedian) }()
|
||||
case 17: try { try decoder.decodeSingularFloatField(value: &_storage._audioRecvPacketLossFraction) }()
|
||||
case 18: try { try decoder.decodeSingularFloatField(value: &_storage._videoRecvPacketLossFraction) }()
|
||||
case 19: try { try decoder.decodeSingularFloatField(value: &_storage._audioSendPacketLossFraction) }()
|
||||
case 20: try { try decoder.decodeSingularFloatField(value: &_storage._videoSendPacketLossFraction) }()
|
||||
case 21: try { try decoder.decodeSingularBytesField(value: &_storage._callTelemetry) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
if _storage._userSatisfied != false {
|
||||
try visitor.visitSingularBoolField(value: _storage._userSatisfied, fieldNumber: 1)
|
||||
}
|
||||
if !_storage._callQualityIssues.isEmpty {
|
||||
try visitor.visitRepeatedStringField(value: _storage._callQualityIssues, fieldNumber: 2)
|
||||
}
|
||||
try { if let v = _storage._additionalIssuesDescription {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||
} }()
|
||||
try { if let v = _storage._debugLogURL {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 4)
|
||||
} }()
|
||||
if _storage._startTimestamp != 0 {
|
||||
try visitor.visitSingularInt64Field(value: _storage._startTimestamp, fieldNumber: 5)
|
||||
}
|
||||
if _storage._endTimestamp != 0 {
|
||||
try visitor.visitSingularInt64Field(value: _storage._endTimestamp, fieldNumber: 6)
|
||||
}
|
||||
if !_storage._callType.isEmpty {
|
||||
try visitor.visitSingularStringField(value: _storage._callType, fieldNumber: 7)
|
||||
}
|
||||
if _storage._success != false {
|
||||
try visitor.visitSingularBoolField(value: _storage._success, fieldNumber: 8)
|
||||
}
|
||||
if !_storage._callEndReason.isEmpty {
|
||||
try visitor.visitSingularStringField(value: _storage._callEndReason, fieldNumber: 9)
|
||||
}
|
||||
try { if let v = _storage._connectionRttMedian {
|
||||
try visitor.visitSingularFloatField(value: v, fieldNumber: 10)
|
||||
} }()
|
||||
try { if let v = _storage._audioRttMedian {
|
||||
try visitor.visitSingularFloatField(value: v, fieldNumber: 11)
|
||||
} }()
|
||||
try { if let v = _storage._videoRttMedian {
|
||||
try visitor.visitSingularFloatField(value: v, fieldNumber: 12)
|
||||
} }()
|
||||
try { if let v = _storage._audioRecvJitterMedian {
|
||||
try visitor.visitSingularFloatField(value: v, fieldNumber: 13)
|
||||
} }()
|
||||
try { if let v = _storage._videoRecvJitterMedian {
|
||||
try visitor.visitSingularFloatField(value: v, fieldNumber: 14)
|
||||
} }()
|
||||
try { if let v = _storage._audioSendJitterMedian {
|
||||
try visitor.visitSingularFloatField(value: v, fieldNumber: 15)
|
||||
} }()
|
||||
try { if let v = _storage._videoSendJitterMedian {
|
||||
try visitor.visitSingularFloatField(value: v, fieldNumber: 16)
|
||||
} }()
|
||||
try { if let v = _storage._audioRecvPacketLossFraction {
|
||||
try visitor.visitSingularFloatField(value: v, fieldNumber: 17)
|
||||
} }()
|
||||
try { if let v = _storage._videoRecvPacketLossFraction {
|
||||
try visitor.visitSingularFloatField(value: v, fieldNumber: 18)
|
||||
} }()
|
||||
try { if let v = _storage._audioSendPacketLossFraction {
|
||||
try visitor.visitSingularFloatField(value: v, fieldNumber: 19)
|
||||
} }()
|
||||
try { if let v = _storage._videoSendPacketLossFraction {
|
||||
try visitor.visitSingularFloatField(value: v, fieldNumber: 20)
|
||||
} }()
|
||||
try { if let v = _storage._callTelemetry {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 21)
|
||||
} }()
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
public static func ==(lhs: CallQualitySurveyProtos_SubmitCallQualitySurveyRequest, rhs: CallQualitySurveyProtos_SubmitCallQualitySurveyRequest) -> Bool {
|
||||
if lhs._storage !== rhs._storage {
|
||||
let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
|
||||
let _storage = _args.0
|
||||
let rhs_storage = _args.1
|
||||
if _storage._userSatisfied != rhs_storage._userSatisfied {return false}
|
||||
if _storage._callQualityIssues != rhs_storage._callQualityIssues {return false}
|
||||
if _storage._additionalIssuesDescription != rhs_storage._additionalIssuesDescription {return false}
|
||||
if _storage._debugLogURL != rhs_storage._debugLogURL {return false}
|
||||
if _storage._startTimestamp != rhs_storage._startTimestamp {return false}
|
||||
if _storage._endTimestamp != rhs_storage._endTimestamp {return false}
|
||||
if _storage._callType != rhs_storage._callType {return false}
|
||||
if _storage._success != rhs_storage._success {return false}
|
||||
if _storage._callEndReason != rhs_storage._callEndReason {return false}
|
||||
if _storage._connectionRttMedian != rhs_storage._connectionRttMedian {return false}
|
||||
if _storage._audioRttMedian != rhs_storage._audioRttMedian {return false}
|
||||
if _storage._videoRttMedian != rhs_storage._videoRttMedian {return false}
|
||||
if _storage._audioRecvJitterMedian != rhs_storage._audioRecvJitterMedian {return false}
|
||||
if _storage._videoRecvJitterMedian != rhs_storage._videoRecvJitterMedian {return false}
|
||||
if _storage._audioSendJitterMedian != rhs_storage._audioSendJitterMedian {return false}
|
||||
if _storage._videoSendJitterMedian != rhs_storage._videoSendJitterMedian {return false}
|
||||
if _storage._audioRecvPacketLossFraction != rhs_storage._audioRecvPacketLossFraction {return false}
|
||||
if _storage._videoRecvPacketLossFraction != rhs_storage._videoRecvPacketLossFraction {return false}
|
||||
if _storage._audioSendPacketLossFraction != rhs_storage._audioSendPacketLossFraction {return false}
|
||||
if _storage._videoSendPacketLossFraction != rhs_storage._videoSendPacketLossFraction {return false}
|
||||
if _storage._callTelemetry != rhs_storage._callTelemetry {return false}
|
||||
return true
|
||||
}
|
||||
if !storagesAreEqual {return false}
|
||||
}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@
|
||||
PROTOC=protoc --proto_path=Specifications
|
||||
WRAPPER_SCRIPT=../../Scripts/protos/ProtoWrappers.py --proto-dir=Specifications --verbose
|
||||
|
||||
all: registration_protos signal_service_protos provisioning_protos fingerprint_protos websocket_protos signal_ios_protos storage_service_protos groups_protos device_transfer_protos session_record_protos svr_protos mobilecoin_protos backup_protos
|
||||
all: registration_protos signal_service_protos provisioning_protos fingerprint_protos websocket_protos signal_ios_protos storage_service_protos groups_protos device_transfer_protos session_record_protos svr_protos mobilecoin_protos backup_protos call_quality_survey_protos
|
||||
|
||||
signal_service_protos: Specifications/SignalService.proto
|
||||
$(PROTOC) --swift_out=Generated SignalService.proto
|
||||
@ -56,3 +56,6 @@ registration_protos: Specifications/Registration.proto
|
||||
|
||||
backup_protos: Backups/Backup.proto
|
||||
$(PROTOC) --swift_out=Backups --proto_path=Backups --swift_opt=Visibility=public --swift_opt=UseAccessLevelOnImports=true Backup.proto
|
||||
|
||||
call_quality_survey_protos: Specifications/CallQualitySurvey.proto
|
||||
$(PROTOC) --swift_out=Generated CallQualitySurvey.proto --swift_opt=Visibility=public --swift_opt=UseAccessLevelOnImports=true
|
||||
|
||||
102
SignalServiceKit/Protos/Specifications/CallQualitySurvey.proto
Normal file
102
SignalServiceKit/Protos/Specifications/CallQualitySurvey.proto
Normal file
@ -0,0 +1,102 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
// iOS - package name determines class prefix
|
||||
package CallQualitySurveyProtos;
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "org.signal.chat.calling.quality";
|
||||
|
||||
message SubmitCallQualitySurveyRequest {
|
||||
// Indicates whether the caller was generally satisfied with the quality of
|
||||
// the call
|
||||
bool user_satisfied = 1;
|
||||
|
||||
// A list of call quality issues selected by the caller
|
||||
repeated string call_quality_issues = 2;
|
||||
|
||||
// A free-form description of any additional issues as written by the caller
|
||||
optional string additional_issues_description = 3;
|
||||
|
||||
// A URL for a set of debug logs associated with the call if the caller chose
|
||||
// to submit debug logs
|
||||
optional string debug_log_url = 4;
|
||||
|
||||
// The time at which the call started in milliseconds since the epoch
|
||||
int64 start_timestamp = 5;
|
||||
|
||||
// The time at which the call ended in milliseconds since the epoch
|
||||
int64 end_timestamp = 6;
|
||||
|
||||
// The type of call; note that direct voice calls can become video calls and
|
||||
// vice versa, and this field indicates which mode was selected at call
|
||||
// initiation time. At the time of writing, expected call types are
|
||||
// "direct_voice", "direct_video", "group", and "call_link".
|
||||
string call_type = 7;
|
||||
|
||||
// Indicates whether the call completed without error or if it terminated
|
||||
// abnormally
|
||||
bool success = 8;
|
||||
|
||||
// A client-defined, but human-readable reason for call termination
|
||||
string call_end_reason = 9;
|
||||
|
||||
// The median round-trip time, measured in milliseconds, for STUN/ICE packets
|
||||
// (i.e. connection maintenance and establishment)
|
||||
optional float connection_rtt_median = 10;
|
||||
|
||||
// The median round-trip time, measured in milliseconds, for RTP/RTCP packets
|
||||
// for audio streams
|
||||
optional float audio_rtt_median = 11;
|
||||
|
||||
// The median round-trip time, measured in milliseconds, for RTP/RTCP packets
|
||||
// for video streams
|
||||
optional float video_rtt_median = 12;
|
||||
|
||||
// The median jitter for audio streams, measured in milliseconds, for the
|
||||
// duration of the call as measured by the client submitting the survey
|
||||
optional float audio_recv_jitter_median = 13;
|
||||
|
||||
// The median jitter for video streams, measured in milliseconds, for the
|
||||
// duration of the call as measured by the client submitting the survey
|
||||
optional float video_recv_jitter_median = 14;
|
||||
|
||||
// The median jitter for audio streams, measured in milliseconds, for the
|
||||
// duration of the call as measured by the remote endpoint in the call (either
|
||||
// the peer of the client submitting the survey in a direct call or the SFU in
|
||||
// a group call)
|
||||
optional float audio_send_jitter_median = 15;
|
||||
|
||||
// The median jitter for video streams, measured in milliseconds, for the
|
||||
// duration of the call as measured by the remote endpoint in the call (either
|
||||
// the peer of the client submitting the survey in a direct call or the SFU in
|
||||
// a group call)
|
||||
optional float video_send_jitter_median = 16;
|
||||
|
||||
// The fraction of audio packets lost over the duration of the call as
|
||||
// measured by the client submitting the survey
|
||||
optional float audio_recv_packet_loss_fraction = 17;
|
||||
|
||||
// The fraction of video packets lost over the duration of the call as
|
||||
// measured by the client submitting the survey
|
||||
optional float video_recv_packet_loss_fraction = 18;
|
||||
|
||||
// The fraction of audio packets lost over the duration of the call as
|
||||
// measured by the remote endpoint in the call (either the peer of the client
|
||||
// submitting the survey in a direct call or the SFU in a group call)
|
||||
optional float audio_send_packet_loss_fraction = 19;
|
||||
|
||||
// The fraction of video packets lost over the duration of the call as
|
||||
// measured by the remote endpoint in the call (either the peer of the client
|
||||
// submitting the survey in a direct call or the SFU in a group call)
|
||||
optional float video_send_packet_loss_fraction = 20;
|
||||
|
||||
// Machine-generated telemetry from the call; this is a serialized protobuf
|
||||
// entity generated (and, critically, explained to the user!) by the calling
|
||||
// library
|
||||
optional bytes call_telemetry = 21;
|
||||
}
|
||||
@ -208,7 +208,11 @@ class ToastView: UIView {
|
||||
label = UILabel()
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.cornerRadius = 12
|
||||
if #available(iOS 26, *) {
|
||||
self.cornerConfiguration = .capsule()
|
||||
} else {
|
||||
self.layer.cornerRadius = 12
|
||||
}
|
||||
self.clipsToBounds = true
|
||||
self.layoutMargins = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user