mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Add initial Call Quality UX.
This commit is contained in:
committed by
Greyson Parrelli
parent
6e0bfa2cee
commit
f38262c0ab
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.quality
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Fragment which manages sheets for walking the user through collecting call
|
||||
* quality feedback.
|
||||
*/
|
||||
class CallQualityBottomSheetFragment : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "CallQualityBottomSheetRequestKey"
|
||||
}
|
||||
|
||||
private val viewModel: CallQualityScreenViewModel by viewModel {
|
||||
CallQualityScreenViewModel()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
setFragmentResultListener(CallQualitySomethingElseFragment.REQUEST_KEY) { key, bundle ->
|
||||
val result = bundle.getString(CallQualitySomethingElseFragment.REQUEST_KEY) ?: ""
|
||||
|
||||
viewModel.onSomethingElseDescriptionChanged(result)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
CallQualitySheet(
|
||||
state = state,
|
||||
callback = remember { Callback() }
|
||||
)
|
||||
}
|
||||
|
||||
private inner class Callback : CallQualitySheetCallback {
|
||||
override fun dismiss() {
|
||||
this@CallQualityBottomSheetFragment.dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun viewDebugLog() {
|
||||
startActivity(
|
||||
Intent(requireContext(), SubmitDebugLogActivity::class.java).apply {
|
||||
putExtra(SubmitDebugLogActivity.ARG_VIEW_ONLY, true)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun describeYourIssue() {
|
||||
CallQualitySomethingElseFragment.create(
|
||||
viewModel.state.value.somethingElseDescription
|
||||
).show(parentFragmentManager, null)
|
||||
}
|
||||
|
||||
override fun onCallQualityIssueSelectionChanged(selection: Set<CallQualityIssue>) {
|
||||
viewModel.onCallQualityIssueSelectionChanged(selection)
|
||||
}
|
||||
|
||||
override fun onShareDebugLogChanged(shareDebugLog: Boolean) {
|
||||
viewModel.onShareDebugLogChanged(shareDebugLog)
|
||||
}
|
||||
|
||||
override fun submit() {
|
||||
viewModel.submit()
|
||||
dismiss()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to true))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.quality
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class CallQualityScreenViewModel : ViewModel() {
|
||||
|
||||
private val internalState = MutableStateFlow(CallQualitySheetState())
|
||||
val state: StateFlow<CallQualitySheetState> = internalState
|
||||
|
||||
fun onCallQualityIssueSelectionChanged(selection: Set<CallQualityIssue>) {
|
||||
internalState.update { it.copy(selectedQualityIssues = selection) }
|
||||
}
|
||||
|
||||
fun onSomethingElseDescriptionChanged(somethingElseDescription: String) {
|
||||
internalState.update { it.copy(somethingElseDescription = somethingElseDescription) }
|
||||
}
|
||||
|
||||
fun onShareDebugLogChanged(shareDebugLog: Boolean) {
|
||||
internalState.update { it.copy(isShareDebugLogSelected = shareDebugLog) }
|
||||
}
|
||||
|
||||
fun submit() {
|
||||
// Enqueue job.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.quality
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.InputChip
|
||||
import androidx.compose.material3.InputChipDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.IconButtons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CallQualitySheet(
|
||||
state: CallQualitySheetState = remember { CallQualitySheetState() },
|
||||
callback: CallQualitySheetCallback = CallQualitySheetCallback.Empty
|
||||
) {
|
||||
var navEntry: CallQualitySheetNavEntry by remember { mutableStateOf(CallQualitySheetNavEntry.HowWasYourCall) }
|
||||
|
||||
when (navEntry) {
|
||||
CallQualitySheetNavEntry.HowWasYourCall -> HowWasYourCall(
|
||||
onGreatClick = {
|
||||
navEntry = CallQualitySheetNavEntry.HelpUsImprove
|
||||
},
|
||||
onHadIssuesClick = {
|
||||
navEntry = CallQualitySheetNavEntry.WhatIssuesDidYouHave
|
||||
},
|
||||
onCancelClick = callback::dismiss
|
||||
)
|
||||
|
||||
CallQualitySheetNavEntry.WhatIssuesDidYouHave -> WhatIssuesDidYouHave(
|
||||
selectedQualityIssues = state.selectedQualityIssues,
|
||||
somethingElseDescription = state.somethingElseDescription,
|
||||
onCallQualityIssueSelectionChanged = callback::onCallQualityIssueSelectionChanged,
|
||||
onContinueClick = {
|
||||
navEntry = CallQualitySheetNavEntry.HelpUsImprove
|
||||
},
|
||||
onDescribeYourIssueClick = callback::describeYourIssue,
|
||||
onCancelClick = callback::dismiss
|
||||
)
|
||||
|
||||
CallQualitySheetNavEntry.HelpUsImprove -> HelpUsImprove(
|
||||
isShareDebugLogSelected = state.isShareDebugLogSelected,
|
||||
onViewDebugLogClick = callback::viewDebugLog,
|
||||
onCancelClick = callback::dismiss,
|
||||
onShareDebugLogChanged = callback::onShareDebugLogChanged,
|
||||
onSubmitClick = callback::submit
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun HowWasYourCall(
|
||||
onHadIssuesClick: () -> Unit,
|
||||
onGreatClick: () -> Unit,
|
||||
onCancelClick: () -> Unit
|
||||
) {
|
||||
Sheet(onDismissRequest = onCancelClick) {
|
||||
SheetTitle(text = stringResource(R.string.CallQualitySheet__how_was_your_call))
|
||||
SheetSubtitle(text = stringResource(R.string.CallQualitySheet__how_was_your_call_subtitle))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp)
|
||||
) {
|
||||
HadIssuesButton(onClick = onHadIssuesClick)
|
||||
GreatButton(onClick = onGreatClick)
|
||||
}
|
||||
|
||||
CancelButton(
|
||||
onClick = onCancelClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = 32.dp, bottom = 24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun WhatIssuesDidYouHave(
|
||||
selectedQualityIssues: Set<CallQualityIssue>,
|
||||
somethingElseDescription: String,
|
||||
onCallQualityIssueSelectionChanged: (Set<CallQualityIssue>) -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onContinueClick: () -> Unit,
|
||||
onDescribeYourIssueClick: () -> Unit
|
||||
) {
|
||||
Sheet(onDismissRequest = onCancelClick) {
|
||||
SheetTitle(text = stringResource(R.string.CallQualitySheet__what_issues_did_you_have))
|
||||
SheetSubtitle(text = stringResource(R.string.CallQualitySheet__select_all_that_apply))
|
||||
|
||||
val qualityIssueDisplaySet = rememberQualityDisplaySet(selectedQualityIssues)
|
||||
val onCallQualityIssueClick: (CallQualityIssue) -> Unit = remember(selectedQualityIssues, onCallQualityIssueSelectionChanged) {
|
||||
{ issue ->
|
||||
val isRemoving = issue in selectedQualityIssues
|
||||
val selection = when {
|
||||
isRemoving && issue == CallQualityIssue.AUDIO_ISSUE -> {
|
||||
selectedQualityIssues.filterNot { it.category == CallQualityIssueCategory.AUDIO }.toSet()
|
||||
}
|
||||
|
||||
isRemoving && issue == CallQualityIssue.VIDEO_ISSUE -> {
|
||||
selectedQualityIssues.filterNot { it.category == CallQualityIssueCategory.VIDEO }.toSet()
|
||||
}
|
||||
|
||||
isRemoving -> {
|
||||
selectedQualityIssues - issue
|
||||
}
|
||||
|
||||
else -> {
|
||||
selectedQualityIssues + issue
|
||||
}
|
||||
}
|
||||
|
||||
onCallQualityIssueSelectionChanged(selection)
|
||||
}
|
||||
}
|
||||
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.animateContentSize()
|
||||
.fillMaxWidth()
|
||||
.horizontalGutters(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
qualityIssueDisplaySet.forEach { issue ->
|
||||
val isIssueSelected = issue in selectedQualityIssues
|
||||
|
||||
InputChip(
|
||||
selected = isIssueSelected,
|
||||
onClick = {
|
||||
onCallQualityIssueClick(issue)
|
||||
},
|
||||
colors = InputChipDefaults.inputChipColors(
|
||||
leadingIconColor = MaterialTheme.colorScheme.onSurface,
|
||||
selectedLeadingIconColor = MaterialTheme.colorScheme.onSurface,
|
||||
labelColor = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = if (isIssueSelected) {
|
||||
ImageVector.vectorResource(R.drawable.symbol_check_24)
|
||||
} else {
|
||||
ImageVector.vectorResource(issue.category.icon)
|
||||
},
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(issue.label))
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (CallQualityIssue.SOMETHING_ELSE in selectedQualityIssues) {
|
||||
val text = somethingElseDescription.ifEmpty {
|
||||
stringResource(R.string.CallQualitySheet__describe_your_issue)
|
||||
}
|
||||
|
||||
val textColor = if (somethingElseDescription.isNotEmpty()) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
|
||||
val textUnderlineStrokeWidthPx = with(LocalDensity.current) {
|
||||
1.dp.toPx()
|
||||
}
|
||||
|
||||
val textUnderlineColor = MaterialTheme.colorScheme.outline
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
role = Role.Button,
|
||||
onClick = onDescribeYourIssueClick
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp)
|
||||
.background(color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
|
||||
val width = size.width
|
||||
val height = size.height - textUnderlineStrokeWidthPx / 2f
|
||||
|
||||
drawLine(
|
||||
color = textUnderlineColor,
|
||||
start = Offset(x = 0f, y = height),
|
||||
end = Offset(x = width, y = height),
|
||||
strokeWidth = textUnderlineStrokeWidthPx
|
||||
)
|
||||
}
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 32.dp, bottom = 24.dp)
|
||||
) {
|
||||
CancelButton(
|
||||
onClick = onCancelClick
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onContinueClick
|
||||
) {
|
||||
Text(text = stringResource(R.string.CallQualitySheet__continue))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun HelpUsImprove(
|
||||
isShareDebugLogSelected: Boolean,
|
||||
onShareDebugLogChanged: (Boolean) -> Unit,
|
||||
onViewDebugLogClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onSubmitClick: () -> Unit
|
||||
) {
|
||||
Sheet(onDismissRequest = onCancelClick) {
|
||||
SheetTitle(text = stringResource(R.string.CallQualitySheet__help_us_improve))
|
||||
SheetSubtitle(
|
||||
text = buildAnnotatedString {
|
||||
append(stringResource(R.string.CallQualitySheet__help_us_improve_description_prefix))
|
||||
append(" ")
|
||||
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
"view-your-debug-log",
|
||||
linkInteractionListener = { onViewDebugLogClick() },
|
||||
styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.primary))
|
||||
)
|
||||
) {
|
||||
append(stringResource(R.string.CallQualitySheet__view_your_debug_log))
|
||||
}
|
||||
|
||||
append(" ")
|
||||
append(stringResource(R.string.CallQualitySheet__help_us_improve_description_suffix))
|
||||
}
|
||||
)
|
||||
|
||||
Rows.ToggleRow(
|
||||
checked = isShareDebugLogSelected,
|
||||
text = stringResource(R.string.CallQualitySheet__share_debug_log),
|
||||
onCheckChanged = onShareDebugLogChanged
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.CallQualitySheet__debug_log_privacy_notice),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.horizontalGutters().padding(top = 14.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalGutters()
|
||||
.padding(top = 32.dp, bottom = 24.dp)
|
||||
) {
|
||||
CancelButton(
|
||||
onClick = onCancelClick
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onSubmitClick
|
||||
) {
|
||||
Text(text = stringResource(R.string.CallQualitySheet__submit))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun Sheet(
|
||||
onDismissRequest: () -> Unit,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dragHandle = null,
|
||||
sheetGesturesEnabled = false,
|
||||
sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SheetTitle(
|
||||
text: String
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 46.dp, bottom = 10.dp)
|
||||
.horizontalGutters()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SheetSubtitle(
|
||||
text: String
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalGutters()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SheetSubtitle(
|
||||
text: AnnotatedString
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalGutters()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HadIssuesButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
FeedbackButton(
|
||||
text = stringResource(R.string.CallQualitySheet__had_issues),
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.error,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GreatButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
FeedbackButton(
|
||||
text = stringResource(R.string.CallQualitySheet__great),
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.primary,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeedbackButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
containerColor: Color,
|
||||
contentColor: Color
|
||||
// imageVector icon
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = spacedBy(12.dp)
|
||||
) {
|
||||
IconButtons.IconButton(
|
||||
onClick = onClick,
|
||||
size = 72.dp,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(color = containerColor)
|
||||
) {
|
||||
// TODO - icon with contentcolor tint
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CancelButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TextButton(onClick = onClick, modifier = modifier) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun CallQualityScreenPreview() {
|
||||
var state by remember { mutableStateOf(CallQualitySheetState()) }
|
||||
|
||||
Previews.Preview {
|
||||
CallQualitySheet(
|
||||
state = state,
|
||||
callback = remember {
|
||||
object : CallQualitySheetCallback by CallQualitySheetCallback.Empty {
|
||||
override fun onCallQualityIssueSelectionChanged(selection: Set<CallQualityIssue>) {
|
||||
state = state.copy(selectedQualityIssues = selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun HowWasYourCallPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
Column {
|
||||
HowWasYourCall(
|
||||
onGreatClick = {},
|
||||
onCancelClick = {},
|
||||
onHadIssuesClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun WhatIssuesDidYouHavePreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
var userSelection by remember { mutableStateOf<Set<CallQualityIssue>>(emptySet()) }
|
||||
|
||||
Column {
|
||||
WhatIssuesDidYouHave(
|
||||
selectedQualityIssues = userSelection,
|
||||
somethingElseDescription = "",
|
||||
onCallQualityIssueSelectionChanged = {
|
||||
userSelection = it
|
||||
},
|
||||
onCancelClick = {},
|
||||
onContinueClick = {},
|
||||
onDescribeYourIssueClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun HelpUsImprovePreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
Column {
|
||||
HelpUsImprove(
|
||||
isShareDebugLogSelected = true,
|
||||
onViewDebugLogClick = {},
|
||||
onCancelClick = {},
|
||||
onShareDebugLogChanged = {},
|
||||
onSubmitClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun SomethingElseContentPreview() {
|
||||
Previews.Preview {
|
||||
CallQualitySomethingElseScreen(
|
||||
somethingElseDescription = "About 5 minutes into a call with my friend",
|
||||
onCancelClick = {},
|
||||
onSaveClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberQualityDisplaySet(userSelection: Set<CallQualityIssue>): Set<CallQualityIssue> {
|
||||
return remember(userSelection) {
|
||||
val displaySet = mutableSetOf<CallQualityIssue>()
|
||||
displaySet.add(CallQualityIssue.AUDIO_ISSUE)
|
||||
if (CallQualityIssue.AUDIO_ISSUE in userSelection) {
|
||||
displaySet.add(CallQualityIssue.AUDIO_STUTTERING)
|
||||
displaySet.add(CallQualityIssue.AUDIO_CUT_OUT)
|
||||
displaySet.add(CallQualityIssue.AUDIO_I_HEARD_ECHO)
|
||||
displaySet.add(CallQualityIssue.AUDIO_OTHERS_HEARD_ECHO)
|
||||
}
|
||||
|
||||
displaySet.add(CallQualityIssue.VIDEO_ISSUE)
|
||||
if (CallQualityIssue.VIDEO_ISSUE in userSelection) {
|
||||
displaySet.add(CallQualityIssue.VIDEO_POOR_QUALITY)
|
||||
displaySet.add(CallQualityIssue.VIDEO_LOW_RESOLUTION)
|
||||
displaySet.add(CallQualityIssue.VIDEO_CAMERA_MALFUNCTION)
|
||||
}
|
||||
|
||||
displaySet.add(CallQualityIssue.CALL_DROPPED)
|
||||
displaySet.add(CallQualityIssue.SOMETHING_ELSE)
|
||||
|
||||
displaySet
|
||||
}
|
||||
}
|
||||
|
||||
data class CallQualitySheetState(
|
||||
val selectedQualityIssues: Set<CallQualityIssue> = emptySet(),
|
||||
val somethingElseDescription: String = "",
|
||||
val isShareDebugLogSelected: Boolean = false
|
||||
)
|
||||
|
||||
interface CallQualitySheetCallback {
|
||||
fun dismiss()
|
||||
fun viewDebugLog()
|
||||
fun describeYourIssue()
|
||||
fun onCallQualityIssueSelectionChanged(selection: Set<CallQualityIssue>)
|
||||
fun onShareDebugLogChanged(shareDebugLog: Boolean)
|
||||
fun submit()
|
||||
|
||||
object Empty : CallQualitySheetCallback {
|
||||
override fun dismiss() = Unit
|
||||
override fun viewDebugLog() = Unit
|
||||
override fun describeYourIssue() = Unit
|
||||
override fun onCallQualityIssueSelectionChanged(selection: Set<CallQualityIssue>) = Unit
|
||||
override fun onShareDebugLogChanged(shareDebugLog: Boolean) = Unit
|
||||
override fun submit() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
private enum class CallQualitySheetNavEntry {
|
||||
HowWasYourCall,
|
||||
WhatIssuesDidYouHave,
|
||||
HelpUsImprove
|
||||
}
|
||||
|
||||
enum class CallQualityIssueCategory(
|
||||
@param:DrawableRes val icon: Int
|
||||
) {
|
||||
AUDIO(icon = R.drawable.symbol_speaker_24),
|
||||
VIDEO(icon = R.drawable.symbol_video_24),
|
||||
CALL_DROPPED(icon = R.drawable.symbol_x_circle_24),
|
||||
SOMETHING_ELSE(icon = R.drawable.symbol_error_circle_24)
|
||||
}
|
||||
|
||||
enum class CallQualityIssue(
|
||||
val category: CallQualityIssueCategory,
|
||||
@param:StringRes val label: Int
|
||||
) {
|
||||
AUDIO_ISSUE(category = CallQualityIssueCategory.AUDIO, label = R.string.CallQualityIssue__audio_issue),
|
||||
AUDIO_STUTTERING(category = CallQualityIssueCategory.AUDIO, label = R.string.CallQualityIssue__audio_stuttering),
|
||||
AUDIO_CUT_OUT(category = CallQualityIssueCategory.AUDIO, label = R.string.CallQualityIssue__audio_cut_out),
|
||||
AUDIO_I_HEARD_ECHO(category = CallQualityIssueCategory.AUDIO, label = R.string.CallQualityIssue__i_heard_echo),
|
||||
AUDIO_OTHERS_HEARD_ECHO(category = CallQualityIssueCategory.AUDIO, label = R.string.CallQualityIssue__others_heard_echo),
|
||||
VIDEO_ISSUE(category = CallQualityIssueCategory.VIDEO, label = R.string.CallQualityIssue__video_issue),
|
||||
VIDEO_POOR_QUALITY(category = CallQualityIssueCategory.VIDEO, label = R.string.CallQualityIssue__poor_video_quality),
|
||||
VIDEO_LOW_RESOLUTION(category = CallQualityIssueCategory.VIDEO, label = R.string.CallQualityIssue__low_resolution),
|
||||
VIDEO_CAMERA_MALFUNCTION(category = CallQualityIssueCategory.VIDEO, label = R.string.CallQualityIssue__camera_did_not_work),
|
||||
CALL_DROPPED(category = CallQualityIssueCategory.CALL_DROPPED, label = R.string.CallQualityIssue__call_droppped),
|
||||
SOMETHING_ELSE(category = CallQualityIssueCategory.SOMETHING_ELSE, label = R.string.CallQualityIssue__something_else)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.quality
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import org.thoughtcrime.securesms.compose.ComposeFullScreenDialogFragment
|
||||
|
||||
/**
|
||||
* Fragment which allows user to enter additional text to describe a call issue.
|
||||
*/
|
||||
class CallQualitySomethingElseFragment : ComposeFullScreenDialogFragment() {
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "CallQualitySomethingElseRequestKey"
|
||||
|
||||
fun create(somethingElseDescription: String): DialogFragment {
|
||||
return CallQualitySomethingElseFragment().apply {
|
||||
arguments = bundleOf(REQUEST_KEY to somethingElseDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
|
||||
dialog.window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DialogContent() {
|
||||
val initialState = remember { requireArguments().getString(REQUEST_KEY) ?: "" }
|
||||
|
||||
CallQualitySomethingElseScreen(
|
||||
somethingElseDescription = initialState,
|
||||
onSaveClick = {
|
||||
dismissAllowingStateLoss()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to it))
|
||||
},
|
||||
onCancelClick = {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.quality
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.TextFields
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
@Composable
|
||||
fun CallQualitySomethingElseScreen(
|
||||
somethingElseDescription: String,
|
||||
onCancelClick: () -> Unit,
|
||||
onSaveClick: (String) -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.CallQualitySomethingElseScreen__title),
|
||||
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
|
||||
onNavigationClick = onCancelClick,
|
||||
navigationContentDescription = stringResource(R.string.CallQualitySomethingElseScreen__back),
|
||||
modifier = Modifier.imePadding()
|
||||
) { paddingValues ->
|
||||
|
||||
var issue by remember { mutableStateOf(somethingElseDescription) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
) {
|
||||
TextFields.TextField(
|
||||
label = {
|
||||
Text(stringResource(R.string.CallQualitySomethingElseScreen__describe_your_issue))
|
||||
},
|
||||
value = issue,
|
||||
minLines = 4,
|
||||
maxLines = 4,
|
||||
onValueChange = {
|
||||
issue = it
|
||||
},
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.fillMaxWidth()
|
||||
.horizontalGutters()
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.CallQualitySomethingElseScreen__privacy_notice),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.horizontalGutters()
|
||||
.padding(top = 24.dp, bottom = 32.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalGutters()
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
CancelButton(
|
||||
onClick = onCancelClick
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onSaveClick(issue) }
|
||||
) {
|
||||
Text(text = stringResource(R.string.CallQualitySomethingElseScreen__save))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,12 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.signal.core.util.AppUtil
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
@@ -23,6 +25,7 @@ import org.signal.core.util.requireString
|
||||
import org.signal.ringrtc.CallManager
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.quality.CallQualityBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
@@ -92,6 +95,12 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
scrollToPosition = SignalStore.internal.lastScrollPosition
|
||||
|
||||
setFragmentResultListener(CallQualityBottomSheetFragment.REQUEST_KEY) { _, bundle ->
|
||||
if (bundle.getBoolean(CallQualityBottomSheetFragment.REQUEST_KEY, false)) {
|
||||
Snackbar.make(requireView(), R.string.CallQualitySheet__thanks_for_your_feedback, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
@@ -583,6 +592,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Display Call Quality Survey UX"),
|
||||
onClick = {
|
||||
CallQualityBottomSheetFragment().show(parentFragmentManager, null)
|
||||
}
|
||||
)
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from("Bandwidth mode"),
|
||||
listItems = CallManager.DataMode.entries.map { it.name }.toTypedArray(),
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ShareCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.util.LinkifyCompat;
|
||||
@@ -52,6 +53,8 @@ public class SubmitDebugLogActivity extends BaseActivity {
|
||||
|
||||
private static final int CODE_SAVE = 24601;
|
||||
|
||||
public static final String ARG_VIEW_ONLY = "args.view_only";
|
||||
|
||||
private WebView logWebView;
|
||||
private SubmitDebugLogViewModel viewModel;
|
||||
|
||||
@@ -335,7 +338,14 @@ public class SubmitDebugLogActivity extends BaseActivity {
|
||||
subscribeToLogLines();
|
||||
});
|
||||
|
||||
submitButton.setOnClickListener(v -> onSubmitClicked());
|
||||
boolean isViewOnly = getIntent().getBooleanExtra(ARG_VIEW_ONLY, false);
|
||||
if (isViewOnly) {
|
||||
submitButton.setText(R.string.SubmitDebugLogActivity_close);
|
||||
submitButton.setOnClickListener(v -> ActivityCompat.finishAfterTransition(this));
|
||||
} else {
|
||||
submitButton.setOnClickListener(v -> onSubmitClicked());
|
||||
}
|
||||
|
||||
scrollToTopButton.setOnClickListener(v -> DebugLogsViewer.scrollToTop(logWebView));
|
||||
scrollToBottomButton.setOnClickListener(v -> DebugLogsViewer.scrollToBottom(logWebView));
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
|
||||
<style name="Signal.DayNight.Dialog.FullScreen.Donate">
|
||||
</style>
|
||||
|
||||
|
||||
<style name="ThemeOverlay.Signal.MaterialAlertDialog" parent="@style/ThemeOverlay.Material3.MaterialAlertDialog">
|
||||
<item name="alertDialogStyle">@style/Signal.MaterialAlertDialog</item>
|
||||
<item name="android:background">@color/signal_colorSurface1</item>
|
||||
|
||||
@@ -2926,6 +2926,8 @@
|
||||
<string name="SubmitDebugLogActivity_success">Success!</string>
|
||||
<string name="SubmitDebugLogActivity_copy_this_url_and_add_it_to_your_issue">Copy this URL and add it to your issue report or support email:\n\n<b>%1$s</b></string>
|
||||
<string name="SubmitDebugLogActivity_share">Share</string>
|
||||
<!-- Displayed when entering activity in view-only mode -->
|
||||
<string name="SubmitDebugLogActivity_close">Close</string>
|
||||
<string name="SubmitDebugLogActivity_this_log_will_be_posted_publicly_online_for_contributors">This log will be posted publicly online for contributors to view. You may examine it before uploading.</string>
|
||||
<!-- Banner message shown while submitting debug log -->
|
||||
<string name="SubmitDebugLogActivity_your_log_will_be_posted_online">When you click Submit, your log will be posted online for 30 days at a unique, unpublished URL. You may Save it locally first.</string>
|
||||
@@ -8851,5 +8853,75 @@
|
||||
<!-- Snackbar text to add a question before creating a poll -->
|
||||
<string name="CreatePollFragment__add_question">Add a question</string>
|
||||
|
||||
<!-- CallQualityIssue -->
|
||||
<!-- Label for general audio issue category -->
|
||||
<string name="CallQualityIssue__audio_issue">Audio issue</string>
|
||||
<!-- Label for general video issue category -->
|
||||
<string name="CallQualityIssue__video_issue">Video issue</string>
|
||||
<!-- Label for call dropped issue -->
|
||||
<string name="CallQualityIssue__call_droppped">Call dropped</string>
|
||||
<!-- Label for other/custom issues not covered by predefined categories -->
|
||||
<string name="CallQualityIssue__something_else">Something else</string>
|
||||
<!-- Label for audio stuttering/choppy audio issue -->
|
||||
<string name="CallQualityIssue__audio_stuttering">Audio stuttering</string>
|
||||
<!-- Label for audio cutting out/dropping issue -->
|
||||
<string name="CallQualityIssue__audio_cut_out">Audio cut out</string>
|
||||
<!-- Label for echo heard by the user -->
|
||||
<string name="CallQualityIssue__i_heard_echo">I heard echo</string>
|
||||
<!-- Label for echo heard by other call participants -->
|
||||
<string name="CallQualityIssue__others_heard_echo">Others heard echo</string>
|
||||
<!-- Label for poor video quality issue -->
|
||||
<string name="CallQualityIssue__poor_video_quality">Poor video quality</string>
|
||||
<!-- Label for low video resolution issue -->
|
||||
<string name="CallQualityIssue__low_resolution">Low resolution</string>
|
||||
<!-- Label for camera malfunction issue -->
|
||||
<string name="CallQualityIssue__camera_did_not_work">Camera didn\'t work</string>
|
||||
|
||||
<!-- CallQualitySomethingElseScreen -->
|
||||
<!-- Title for the "Something else" call quality issue screen -->
|
||||
<string name="CallQualitySomethingElseScreen__title">Something else</string>
|
||||
<!-- Label for text field where user describes their call quality issue -->
|
||||
<string name="CallQualitySomethingElseScreen__describe_your_issue">Describe your issue</string>
|
||||
<!-- Privacy notice explaining that issue details will be kept private and used to improve Signal calls -->
|
||||
<string name="CallQualitySomethingElseScreen__privacy_notice">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.</string>
|
||||
<!-- Button to save the call quality issue description -->
|
||||
<string name="CallQualitySomethingElseScreen__save">Save</string>
|
||||
<!-- Content description for the back navigation button -->
|
||||
<string name="CallQualitySomethingElseScreen__back">Back</string>
|
||||
|
||||
<!-- CallQualitySheet -->
|
||||
<!-- Title asking user how their call was -->
|
||||
<string name="CallQualitySheet__how_was_your_call">How was your call?</string>
|
||||
<!-- Subtitle explaining that feedback helps improve Signal and no PII is stored -->
|
||||
<string name="CallQualitySheet__how_was_your_call_subtitle">This helps us improve calls in Signal. No personally identifiable information will be stored.</string>
|
||||
<!-- Button label indicating the call had issues -->
|
||||
<string name="CallQualitySheet__had_issues">Had issues</string>
|
||||
<!-- Button label indicating the call was great -->
|
||||
<string name="CallQualitySheet__great">Great</string>
|
||||
<!-- Title asking what issues the user experienced -->
|
||||
<string name="CallQualitySheet__what_issues_did_you_have">What issues did you have?</string>
|
||||
<!-- Instruction to select all applicable issues -->
|
||||
<string name="CallQualitySheet__select_all_that_apply">Select all that apply</string>
|
||||
<!-- Placeholder text for describing a custom issue -->
|
||||
<string name="CallQualitySheet__describe_your_issue">Describe your issue</string>
|
||||
<!-- Button to continue to the next step -->
|
||||
<string name="CallQualitySheet__continue">Continue</string>
|
||||
<!-- Title asking user to help improve Signal -->
|
||||
<string name="CallQualitySheet__help_us_improve">Help us improve</string>
|
||||
<!-- First part of description explaining diagnostics help improve call quality -->
|
||||
<string name="CallQualitySheet__help_us_improve_description_prefix">Sending us your diagnostics info helps us improve call quality. You can </string>
|
||||
<!-- Link text to view debug log -->
|
||||
<string name="CallQualitySheet__view_your_debug_log">view your debug log</string>
|
||||
<!-- Last part of description about viewing debug log before submitting -->
|
||||
<string name="CallQualitySheet__help_us_improve_description_suffix"> before submitting</string>
|
||||
<!-- Toggle label for sharing debug log -->
|
||||
<string name="CallQualitySheet__share_debug_log">Share debug log</string>
|
||||
<!-- Privacy notice explaining what debug logs contain and why they are helpful -->
|
||||
<string name="CallQualitySheet__debug_log_privacy_notice">Debug logs contain low level app information and do not reveal any of your message contents.</string>
|
||||
<!-- Button to submit the call quality feedback -->
|
||||
<string name="CallQualitySheet__submit">Submit</string>
|
||||
<!-- Displayed as a snackbar after submitting feedback -->
|
||||
<string name="CallQualitySheet__thanks_for_your_feedback">Thanks for your feedback!</string>
|
||||
|
||||
<!-- EOF -->
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user