Convert HelpFragment to Compose.
Co-authored-by: Alex Hart <alex@signal.org>
This commit is contained in:
parent
7cce504f16
commit
595364b522
@ -1,270 +0,0 @@
|
||||
package org.thoughtcrime.securesms.help;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
|
||||
import org.signal.core.util.ResourceUtil;
|
||||
import org.signal.core.ui.logging.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.signal.core.util.Util;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class HelpFragment extends LoggingFragment {
|
||||
|
||||
public static final String START_CATEGORY_INDEX = "start_category_index";
|
||||
public static final int PAYMENT_INDEX = 6;
|
||||
public static final int DONATION_INDEX = 7;
|
||||
public static final int REMOTE_BACKUPS_INDEX = 8;
|
||||
|
||||
private EditText problem;
|
||||
private CheckBox includeDebugLogs;
|
||||
private View debugLogInfo;
|
||||
private View faq;
|
||||
private CircularProgressMaterialButton next;
|
||||
private View toaster;
|
||||
private List<EmojiImageView> emoji;
|
||||
private HelpViewModel helpViewModel;
|
||||
private Spinner categorySpinner;
|
||||
private ArrayAdapter<CharSequence> categoryAdapter;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.help_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
initializeViewModels();
|
||||
initializeToolbar(view);
|
||||
initializeViews(view);
|
||||
initializeListeners();
|
||||
initializeObservers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
next.cancelSpinning();
|
||||
problem.setEnabled(true);
|
||||
}
|
||||
|
||||
private void initializeViewModels() {
|
||||
helpViewModel = new ViewModelProvider(this).get(HelpViewModel.class);
|
||||
}
|
||||
|
||||
private void initializeViews(@NonNull View view) {
|
||||
problem = view.findViewById(R.id.help_fragment_problem);
|
||||
includeDebugLogs = view.findViewById(R.id.help_fragment_debug);
|
||||
debugLogInfo = view.findViewById(R.id.help_fragment_debug_info);
|
||||
faq = view.findViewById(R.id.help_fragment_faq);
|
||||
next = view.findViewById(R.id.help_fragment_next);
|
||||
toaster = view.findViewById(R.id.help_fragment_next_toaster);
|
||||
categorySpinner = view.findViewById(R.id.help_fragment_category);
|
||||
emoji = new ArrayList<>(Feeling.values().length);
|
||||
|
||||
for (Feeling feeling : Feeling.values()) {
|
||||
EmojiImageView emojiView = view.findViewById(feeling.getViewId());
|
||||
emojiView.setImageEmoji(feeling.getEmojiCode());
|
||||
emoji.add(view.findViewById(feeling.getViewId()));
|
||||
}
|
||||
|
||||
categoryAdapter = ArrayAdapter.createFromResource(requireContext(), R.array.HelpFragment__categories_6, android.R.layout.simple_spinner_item);
|
||||
categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
||||
categorySpinner.setAdapter(categoryAdapter);
|
||||
|
||||
Bundle args = getArguments();
|
||||
if (args != null) {
|
||||
categorySpinner.setSelection(Util.clamp(args.getInt(START_CATEGORY_INDEX, 0), 0, categorySpinner.getCount() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeListeners() {
|
||||
problem.addTextChangedListener(new AfterTextChanged(e -> helpViewModel.onProblemChanged(e.toString())));
|
||||
emoji.stream().forEach(view -> view.setOnClickListener(this::handleEmojiClicked));
|
||||
faq.setOnClickListener(v -> launchFaq());
|
||||
debugLogInfo.setOnClickListener(v -> launchDebugLogInfo());
|
||||
next.setOnClickListener(v -> submitForm());
|
||||
toaster.setOnClickListener(v -> {
|
||||
if (helpViewModel.getCategoryIndex() == 0) {
|
||||
categorySpinner.startAnimation(AnimationUtils.loadAnimation(requireContext(), R.anim.shake_horizontal));
|
||||
}
|
||||
|
||||
Toast.makeText(requireContext(), R.string.HelpFragment__please_be_as_descriptive_as_possible, Toast.LENGTH_LONG).show();
|
||||
});
|
||||
categorySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
helpViewModel.onCategorySelected(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeObservers() {
|
||||
//noinspection CodeBlock2Expr
|
||||
helpViewModel.isFormValid().observe(getViewLifecycleOwner(), isValid -> {
|
||||
next.setEnabled(isValid);
|
||||
toaster.setVisibility(isValid ? View.GONE : View.VISIBLE);
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeToolbar(@NonNull View view) {
|
||||
Toolbar toolbar = view.findViewById(R.id.toolbar);
|
||||
toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(v).navigateUp());
|
||||
}
|
||||
|
||||
private void handleEmojiClicked(@NonNull View clicked) {
|
||||
if (clicked.isSelected()) {
|
||||
clicked.setSelected(false);
|
||||
} else {
|
||||
emoji.stream().forEach(view -> view.setSelected(false));
|
||||
clicked.setSelected(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void launchFaq() {
|
||||
Uri data = Uri.parse(getString(R.string.HelpFragment__link__faq));
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, data);
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void launchDebugLogInfo() {
|
||||
Uri data = Uri.parse(getString(R.string.HelpFragment__link__debug_info));
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, data);
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void submitForm() {
|
||||
next.setSpinning();
|
||||
problem.setEnabled(false);
|
||||
|
||||
helpViewModel.onSubmitClicked(includeDebugLogs.isChecked()).observe(getViewLifecycleOwner(), result -> {
|
||||
if (result.getDebugLogUrl().isPresent()) {
|
||||
submitFormWithDebugLog(result.getDebugLogUrl().get());
|
||||
} else if (result.isError()) {
|
||||
submitFormWithDebugLog(getString(R.string.HelpFragment__could_not_upload_logs));
|
||||
} else {
|
||||
submitFormWithDebugLog(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void submitFormWithDebugLog(@Nullable String debugLog) {
|
||||
Feeling feeling = emoji.stream()
|
||||
.filter(View::isSelected)
|
||||
.map(view -> Feeling.getByViewId(view.getId()))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
|
||||
CommunicationActions.openEmail(requireContext(),
|
||||
SupportEmailUtil.getSupportEmailAddress(requireContext()),
|
||||
getEmailSubject(),
|
||||
getEmailBody(debugLog, feeling));
|
||||
}
|
||||
|
||||
private String getEmailSubject() {
|
||||
return getString(R.string.HelpFragment__signal_android_support_request);
|
||||
}
|
||||
|
||||
private String getEmailBody(@Nullable String debugLog, @Nullable Feeling feeling) {
|
||||
StringBuilder suffix = new StringBuilder();
|
||||
|
||||
if (debugLog != null) {
|
||||
suffix.append("\n");
|
||||
suffix.append(getString(R.string.HelpFragment__debug_log));
|
||||
suffix.append(" ");
|
||||
suffix.append(debugLog);
|
||||
}
|
||||
|
||||
if (feeling != null) {
|
||||
suffix.append("\n\n");
|
||||
suffix.append(feeling.getEmojiCode());
|
||||
suffix.append("\n");
|
||||
suffix.append(getString(feeling.getStringId()));
|
||||
}
|
||||
|
||||
String[] englishCategories = ResourceUtil.getEnglishResources(requireContext()).getStringArray(R.array.HelpFragment__categories_6);
|
||||
String category = (helpViewModel.getCategoryIndex() >= 0 && helpViewModel.getCategoryIndex() < englishCategories.length) ? englishCategories[helpViewModel.getCategoryIndex()]
|
||||
: categoryAdapter.getItem(helpViewModel.getCategoryIndex()).toString();
|
||||
|
||||
return SupportEmailUtil.generateSupportEmailBody(requireContext(),
|
||||
R.string.HelpFragment__signal_android_support_request,
|
||||
" - " + category,
|
||||
problem.getText().toString() + "\n\n",
|
||||
suffix.toString());
|
||||
}
|
||||
|
||||
private enum Feeling {
|
||||
ECSTATIC(R.id.help_fragment_emoji_5, R.string.HelpFragment__emoji_5, "\ud83d\ude00"),
|
||||
HAPPY(R.id.help_fragment_emoji_4, R.string.HelpFragment__emoji_4, "\ud83d\ude42"),
|
||||
AMBIVALENT(R.id.help_fragment_emoji_3, R.string.HelpFragment__emoji_3, "\ud83d\ude10"),
|
||||
UNHAPPY(R.id.help_fragment_emoji_2, R.string.HelpFragment__emoji_2, "\ud83d\ude41"),
|
||||
ANGRY(R.id.help_fragment_emoji_1, R.string.HelpFragment__emoji_1, "\ud83d\ude20");
|
||||
|
||||
private final @IdRes int viewId;
|
||||
private final @StringRes int stringId;
|
||||
private final CharSequence emojiCode;
|
||||
|
||||
Feeling(@IdRes int viewId, @StringRes int stringId, @NonNull CharSequence emojiCode) {
|
||||
this.viewId = viewId;
|
||||
this.stringId = stringId;
|
||||
this.emojiCode = emojiCode;
|
||||
}
|
||||
|
||||
public @IdRes int getViewId() {
|
||||
return viewId;
|
||||
}
|
||||
|
||||
public @StringRes int getStringId() {
|
||||
return stringId;
|
||||
}
|
||||
|
||||
public @NonNull CharSequence getEmojiCode() {
|
||||
return emojiCode;
|
||||
}
|
||||
|
||||
static Feeling getByViewId(@IdRes int viewId) {
|
||||
for (Feeling feeling : values()) {
|
||||
if (feeling.viewId == viewId) {
|
||||
return feeling;
|
||||
}
|
||||
}
|
||||
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.help
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.signal.core.ui.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
class HelpFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: HelpViewModel by viewModel {
|
||||
val categoryCount = resources.getStringArray(R.array.HelpFragment__categories_6).size
|
||||
HelpViewModel(
|
||||
startCategoryIndex = (arguments?.getInt(START_CATEGORY_INDEX, 0) ?: 0).coerceIn(0, categoryCount - 1),
|
||||
application = AppDependencies.application
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.onScreenResumed()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
HelpScreenContent(
|
||||
state = state,
|
||||
onEvent = { event ->
|
||||
when (event) {
|
||||
HelpScreenEvents.NavigationClick -> { requireActivity().onBackPressedDispatcher.onBackPressed() }
|
||||
HelpScreenEvents.FAQClick -> {
|
||||
CommunicationActions.openBrowserLink(
|
||||
requireContext(),
|
||||
getString(R.string.HelpFragment__link__faq)
|
||||
)
|
||||
}
|
||||
HelpScreenEvents.WhatIsDebugLogClick -> {
|
||||
CommunicationActions.openBrowserLink(
|
||||
requireContext(),
|
||||
getString(R.string.HelpFragment__link__debug_info)
|
||||
)
|
||||
}
|
||||
else -> viewModel.onEvent(event)
|
||||
}
|
||||
},
|
||||
sideEffect = viewModel.sideEffect
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val START_CATEGORY_INDEX = "start_category_index"
|
||||
const val PAYMENT_INDEX = 6
|
||||
const val DONATION_INDEX = 7
|
||||
const val REMOTE_BACKUPS_INDEX = 8
|
||||
}
|
||||
}
|
||||
401
app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt
Normal file
401
app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt
Normal file
@ -0,0 +1,401 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.help
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.keyframes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.CircularProgressWrapper
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.compose.Snackbars
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImage
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
|
||||
@Composable
|
||||
fun HelpScreenContent(
|
||||
state: HelpScreenState,
|
||||
onEvent: (HelpScreenEvents) -> Unit,
|
||||
sideEffect: Flow<HelpScreenSideEffects>
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val categories = stringArrayResource(R.array.HelpFragment__categories_6).toList()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val categoryShakeOffset = remember { Animatable(0f) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
sideEffect.collect { sideEffect ->
|
||||
when (sideEffect) {
|
||||
is HelpScreenSideEffects.OpenEmail -> {
|
||||
CommunicationActions.openEmail(
|
||||
context,
|
||||
SupportEmailUtil.getSupportEmailAddress(context),
|
||||
sideEffect.subject,
|
||||
sideEffect.body
|
||||
)
|
||||
}
|
||||
|
||||
is HelpScreenSideEffects.ShowSnackbar -> {
|
||||
snackbarHostState.showSnackbar(sideEffect.getMessage(context))
|
||||
}
|
||||
|
||||
HelpScreenSideEffects.ShakeCategory -> {
|
||||
scope.launch {
|
||||
categoryShakeOffset.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = keyframes {
|
||||
durationMillis = 300
|
||||
0f at 0
|
||||
-8f at 50
|
||||
8f at 100
|
||||
-8f at 150
|
||||
8f at 200
|
||||
-8f at 250
|
||||
0f at 300
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffolds.Settings(
|
||||
snackbarHost = { Snackbars.Host(snackbarHostState = snackbarHostState) },
|
||||
title = stringResource(R.string.preferences__help),
|
||||
onNavigationClick = { onEvent(HelpScreenEvents.NavigationClick) },
|
||||
navigationIcon = SignalIcons.ArrowStart.imageVector
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.horizontalGutters()
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 12.dp),
|
||||
text = stringResource(id = R.string.HelpFragment__contact_us),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
val isFieldError = state.displayValidationErrors && !state.isTextValid
|
||||
TextField(
|
||||
isError = isFieldError,
|
||||
supportingText = {
|
||||
if (isFieldError) {
|
||||
Text(text = pluralStringResource(R.plurals.HelpFragment__must_be_at_least_n_characters, HelpScreenState.MINIMUM_PROBLEM_CHARS, HelpScreenState.MINIMUM_PROBLEM_CHARS))
|
||||
}
|
||||
},
|
||||
value = state.problemText,
|
||||
onValueChange = { onEvent(HelpScreenEvents.ProblemTextChanged(it)) },
|
||||
placeholder = {
|
||||
Text(text = stringResource(id = R.string.HelpFragment__tell_us_whats_going_on))
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
maxLines = Int.MAX_VALUE,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 144.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
text = stringResource(id = R.string.HelpFragment__tell_us_why_youre_reaching_out),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
CategoryDropdown(
|
||||
modifier = Modifier
|
||||
.offset { IntOffset(x = categoryShakeOffset.value.dp.roundToPx(), y = 0) }
|
||||
.padding(bottom = 16.dp)
|
||||
.let { modifier ->
|
||||
if (state.displayValidationErrors && !state.isCategoryValid) {
|
||||
modifier.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = 4.dp,
|
||||
topEnd = 4.dp,
|
||||
bottomStart = 0.dp,
|
||||
bottomEnd = 0.dp
|
||||
)
|
||||
)
|
||||
} else {
|
||||
modifier
|
||||
}
|
||||
},
|
||||
categories = categories,
|
||||
selectedIndex = state.categoryIndex,
|
||||
onCategorySelected = { onEvent(HelpScreenEvents.CategorySelected(it)) }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 12.dp),
|
||||
text = stringResource(id = R.string.HelpFragment__how_do_you_feel),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
EmojiRatingRow(
|
||||
modifier = Modifier.padding(bottom = 12.dp),
|
||||
selectedFeeling = state.selectedFeeling,
|
||||
onFeelingSelected = { onEvent(HelpScreenEvents.FeelingSelected(it)) }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = state.includeDebugLog,
|
||||
onCheckedChange = { onEvent(HelpScreenEvents.DebugLogsToggled(it)) }
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.HelpFragment__include_debug_log),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
TextButton(onClick = { onEvent(HelpScreenEvents.WhatIsDebugLogClick) }) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.HelpFragment__whats_this),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp, start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
text = buildAnnotatedString {
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
"view-faq",
|
||||
linkInteractionListener = { onEvent(HelpScreenEvents.FAQClick) },
|
||||
styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.primary))
|
||||
)
|
||||
) {
|
||||
append(stringResource(R.string.HelpFragment__have_you_read_our_faq_yet))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
CircularProgressWrapper(
|
||||
isLoading = state.isSubmitting
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = { onEvent(HelpScreenEvents.OnNextClick) },
|
||||
enabled = !state.isSubmitting
|
||||
) {
|
||||
Text(stringResource(R.string.HelpFragment__next))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CategoryDropdown(
|
||||
categories: List<String>,
|
||||
selectedIndex: Int,
|
||||
onCategorySelected: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
modifier = modifier,
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded }
|
||||
) {
|
||||
TextField(
|
||||
modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, true),
|
||||
value = categories.getOrElse(selectedIndex) { "" },
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
categories.forEachIndexed { index, category ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(category) },
|
||||
onClick = {
|
||||
onCategorySelected(index)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmojiRatingRow(
|
||||
selectedFeeling: Feeling?,
|
||||
onFeelingSelected: (Feeling) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Feeling.entries.forEach { feeling ->
|
||||
EmojiButton(
|
||||
feeling = feeling,
|
||||
isSelected = feeling == selectedFeeling,
|
||||
onClick = { onFeelingSelected(feeling) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmojiButton(
|
||||
feeling: Feeling,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
|
||||
val backgroundColor = if (isSelected) {
|
||||
if (isDark) Color(0xFF6191f3) else Color(0xFF2C6BED)
|
||||
} else {
|
||||
if (isDark) Color(0xFF3b3b3b) else Color(0xFFE9E9E9)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(backgroundColor, shape = CircleShape)
|
||||
.padding(4.dp)
|
||||
.clickable(onClick = onClick),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
EmojiImage(
|
||||
emoji = feeling.emojiCode,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Feeling(val emojiCode: String, val labelRes: Int) {
|
||||
ECSTATIC(emojiCode = "\ud83d\ude00", labelRes = R.string.HelpFragment__emoji_5),
|
||||
HAPPY(emojiCode = "\ud83d\ude42", labelRes = R.string.HelpFragment__emoji_4),
|
||||
AMBIVALENT(emojiCode = "\ud83d\ude10", labelRes = R.string.HelpFragment__emoji_3),
|
||||
UNHAPPY(emojiCode = "\ud83d\ude41", labelRes = R.string.HelpFragment__emoji_2),
|
||||
ANGRY(emojiCode = "\ud83d\ude20", labelRes = R.string.HelpFragment__emoji_1)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun HelpScreenPreview() {
|
||||
Previews.Preview {
|
||||
HelpScreenContent(
|
||||
state = HelpScreenState(),
|
||||
onEvent = {},
|
||||
sideEffect = emptyFlow()
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.help
|
||||
|
||||
sealed interface HelpScreenEvents {
|
||||
data class ProblemTextChanged(val text: String) : HelpScreenEvents
|
||||
data class CategorySelected(val index: Int) : HelpScreenEvents
|
||||
data class FeelingSelected(val feeling: Feeling) : HelpScreenEvents
|
||||
data class DebugLogsToggled(val toggle: Boolean) : HelpScreenEvents
|
||||
data object OnNextClick : HelpScreenEvents
|
||||
data object NavigationClick : HelpScreenEvents
|
||||
data object WhatIsDebugLogClick : HelpScreenEvents
|
||||
data object FAQClick : HelpScreenEvents
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.help
|
||||
|
||||
import android.content.Context
|
||||
|
||||
sealed interface HelpScreenSideEffects {
|
||||
data class OpenEmail(val subject: String, val body: String) : HelpScreenSideEffects
|
||||
data class ShowSnackbar(val messageRes: Int) : HelpScreenSideEffects {
|
||||
fun getMessage(context: Context): String = context.getString(messageRes)
|
||||
}
|
||||
data object ShakeCategory : HelpScreenSideEffects
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.help
|
||||
|
||||
data class HelpScreenState(
|
||||
val problemText: String = "",
|
||||
val categoryIndex: Int = 0,
|
||||
val selectedFeeling: Feeling? = null,
|
||||
val includeDebugLog: Boolean = true,
|
||||
val isSubmitting: Boolean = false,
|
||||
val displayValidationErrors: Boolean = false
|
||||
) {
|
||||
val isTextValid: Boolean = problemText.length >= MINIMUM_PROBLEM_CHARS
|
||||
val isCategoryValid: Boolean = categoryIndex > 0
|
||||
|
||||
val isFormValid: Boolean = isTextValid && isCategoryValid
|
||||
|
||||
companion object {
|
||||
const val MINIMUM_PROBLEM_CHARS = 10
|
||||
}
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
package org.thoughtcrime.securesms.help;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public class HelpViewModel extends ViewModel {
|
||||
|
||||
private static final int MINIMUM_PROBLEM_CHARS = 10;
|
||||
|
||||
private final MutableLiveData<Boolean> problemMeetsLengthRequirements;
|
||||
private final MutableLiveData<Integer> categoryIndex;
|
||||
private final LiveData<Boolean> isFormValid;
|
||||
|
||||
private final SubmitDebugLogRepository submitDebugLogRepository;
|
||||
|
||||
public HelpViewModel() {
|
||||
submitDebugLogRepository = new SubmitDebugLogRepository();
|
||||
problemMeetsLengthRequirements = new MutableLiveData<>();
|
||||
categoryIndex = new MutableLiveData<>(0);
|
||||
|
||||
isFormValid = LiveDataUtil.combineLatest(problemMeetsLengthRequirements, categoryIndex, (meetsLengthRequirements, index) -> {
|
||||
return meetsLengthRequirements == Boolean.TRUE && index > 0;
|
||||
});
|
||||
}
|
||||
|
||||
LiveData<Boolean> isFormValid() {
|
||||
return isFormValid;
|
||||
}
|
||||
|
||||
void onProblemChanged(@NonNull String problem) {
|
||||
problemMeetsLengthRequirements.setValue(problem.length() >= MINIMUM_PROBLEM_CHARS);
|
||||
}
|
||||
|
||||
void onCategorySelected(int index) {
|
||||
this.categoryIndex.setValue(index);
|
||||
}
|
||||
|
||||
int getCategoryIndex() {
|
||||
return Optional.ofNullable(this.categoryIndex.getValue()).orElse(0);
|
||||
}
|
||||
|
||||
LiveData<SubmitResult> onSubmitClicked(boolean includeDebugLogs) {
|
||||
MutableLiveData<SubmitResult> resultLiveData = new MutableLiveData<>();
|
||||
|
||||
if (includeDebugLogs) {
|
||||
submitDebugLogRepository.buildAndSubmitLog(result -> resultLiveData.postValue(new SubmitResult(result, result.isPresent())));
|
||||
} else {
|
||||
resultLiveData.postValue(new SubmitResult(Optional.empty(), false));
|
||||
}
|
||||
|
||||
return resultLiveData;
|
||||
}
|
||||
|
||||
static class SubmitResult {
|
||||
private final Optional<String> debugLogUrl;
|
||||
private final boolean isError;
|
||||
|
||||
private SubmitResult(@NonNull Optional<String> debugLogUrl, boolean isError) {
|
||||
this.debugLogUrl = debugLogUrl;
|
||||
this.isError = isError;
|
||||
}
|
||||
|
||||
@NonNull Optional<String> getDebugLogUrl() {
|
||||
return debugLogUrl;
|
||||
}
|
||||
|
||||
boolean isError() {
|
||||
return isError;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.help
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.application
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.ResourceUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
|
||||
class HelpViewModel(
|
||||
startCategoryIndex: Int,
|
||||
application: Application
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
private val internalState = MutableStateFlow(
|
||||
HelpScreenState(
|
||||
categoryIndex = startCategoryIndex
|
||||
)
|
||||
)
|
||||
val state = internalState.asStateFlow()
|
||||
|
||||
private val internalSideEffect = Channel<HelpScreenSideEffects>(Channel.BUFFERED)
|
||||
val sideEffect = internalSideEffect.receiveAsFlow()
|
||||
|
||||
private val submitDebugLogRepository = SubmitDebugLogRepository()
|
||||
|
||||
fun onEvent(event: HelpScreenEvents) {
|
||||
when (event) {
|
||||
is HelpScreenEvents.ProblemTextChanged -> onProblemChanged(event.text)
|
||||
is HelpScreenEvents.CategorySelected -> onCategorySelected(event.index)
|
||||
is HelpScreenEvents.FeelingSelected -> onFeelingSelected(event.feeling)
|
||||
is HelpScreenEvents.DebugLogsToggled -> onDebugLogsToggled(event.toggle)
|
||||
is HelpScreenEvents.OnNextClick -> onNextClick()
|
||||
else -> error("Unhandled event: $event")
|
||||
}
|
||||
}
|
||||
|
||||
fun onScreenResumed() {
|
||||
internalState.update { it.copy(isSubmitting = false) }
|
||||
}
|
||||
|
||||
private fun onProblemChanged(text: String) {
|
||||
internalState.update { it.copy(problemText = text) }
|
||||
}
|
||||
|
||||
private fun onCategorySelected(index: Int) {
|
||||
internalState.update { it.copy(categoryIndex = index) }
|
||||
}
|
||||
|
||||
private fun onFeelingSelected(feeling: Feeling) {
|
||||
internalState.update { current ->
|
||||
current.copy(selectedFeeling = if (current.selectedFeeling == feeling) null else feeling)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDebugLogsToggled(include: Boolean) {
|
||||
internalState.update { it.copy(includeDebugLog = include) }
|
||||
}
|
||||
|
||||
private fun onNextClick() {
|
||||
if (!state.value.isFormValid) {
|
||||
internalState.update { it.copy(displayValidationErrors = true) }
|
||||
viewModelScope.launch {
|
||||
if (state.value.categoryIndex <= 0) {
|
||||
internalSideEffect.send(HelpScreenSideEffects.ShakeCategory)
|
||||
}
|
||||
internalSideEffect.send(HelpScreenSideEffects.ShowSnackbar(R.string.HelpFragment__please_be_as_descriptive_as_possible))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
if (internalState.value.includeDebugLog) {
|
||||
internalState.update { it.copy(isSubmitting = true) }
|
||||
|
||||
submitDebugLogRepository.buildAndSubmitLog { optionalUrl ->
|
||||
val debugLogUrl = if (optionalUrl.isPresent) optionalUrl.get()
|
||||
else application.getString(R.string.HelpFragment__could_not_upload_logs)
|
||||
|
||||
dispatchEmail(debugLogUrl)
|
||||
}
|
||||
} else {
|
||||
dispatchEmail(debugLogUrl = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchEmail(debugLogUrl: String?) {
|
||||
val context = application
|
||||
val state = internalState.value
|
||||
val englishCategories: Array<String> = ResourceUtil.getEnglishResources(context)
|
||||
.getStringArray(R.array.HelpFragment__categories_6)
|
||||
val categoryLabel = englishCategories.getOrElse(state.categoryIndex) { "" }
|
||||
|
||||
val suffix = buildString {
|
||||
if (debugLogUrl != null) {
|
||||
append("\n")
|
||||
append(context.getString(R.string.HelpFragment__debug_log))
|
||||
append(" ")
|
||||
append(debugLogUrl)
|
||||
}
|
||||
state.selectedFeeling?.let { feeling ->
|
||||
append("\n\n")
|
||||
append(feeling.emojiCode)
|
||||
append("\n")
|
||||
append(context.getString(feeling.labelRes))
|
||||
}
|
||||
}
|
||||
|
||||
val subject = context.getString(R.string.HelpFragment__signal_android_support_request)
|
||||
val body = SupportEmailUtil.generateSupportEmailBody(
|
||||
context,
|
||||
R.string.HelpFragment__signal_android_support_request,
|
||||
" - $categoryLabel",
|
||||
"${state.problemText}\n\n",
|
||||
suffix
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
internalSideEffect.send(HelpScreenSideEffects.OpenEmail(subject = subject, body = body))
|
||||
internalState.update { it.copy(isSubmitting = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true">
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/signal_button_secondary_text" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/signal_button_secondary" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@ -1,261 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/help_fragment_scroller"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:fadingEdge="vertical"
|
||||
android:fillViewport="true"
|
||||
android:requiresFadingEdge="vertical"
|
||||
app:layout_constraintBottom_toTopOf="@id/help_fragment_faq"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navigationIcon="@drawable/symbol_arrow_start_24"
|
||||
app:title="@string/preferences__help" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/help_fragment_contact"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/HelpFragment__contact_us"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/help_fragment_problem"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/help_fragment_problem_background"
|
||||
android:gravity="top"
|
||||
android:hint="@string/HelpFragment__tell_us_whats_going_on"
|
||||
android:inputType="textMultiLine"
|
||||
android:minHeight="144dp"
|
||||
android:padding="16dp"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/help_fragment_contact" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/help_fragment_category_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/HelpFragment__tell_us_why_youre_reaching_out"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/help_fragment_problem" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/help_fragment_category"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/help_fragment_category_title"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/help_fragment_feelings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/HelpFragment__how_do_you_feel"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/help_fragment_category" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/help_fragment_emoji_5"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/help_fragment_emoji_radio_background"
|
||||
android:button="@null"
|
||||
android:gravity="center"
|
||||
android:padding="4dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintEnd_toStartOf="@id/help_fragment_emoji_4"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/help_fragment_feelings" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/help_fragment_emoji_4"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/help_fragment_emoji_radio_background"
|
||||
android:button="@null"
|
||||
android:gravity="center"
|
||||
android:scaleType="fitCenter"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/help_fragment_emoji_5"
|
||||
app:layout_constraintEnd_toStartOf="@id/help_fragment_emoji_3"
|
||||
app:layout_constraintStart_toEndOf="@id/help_fragment_emoji_5"
|
||||
app:layout_constraintTop_toTopOf="@id/help_fragment_emoji_5" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/help_fragment_emoji_3"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/help_fragment_emoji_radio_background"
|
||||
android:button="@null"
|
||||
android:gravity="center"
|
||||
android:scaleType="fitCenter"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/help_fragment_emoji_4"
|
||||
app:layout_constraintEnd_toStartOf="@id/help_fragment_emoji_2"
|
||||
app:layout_constraintStart_toEndOf="@id/help_fragment_emoji_4"
|
||||
app:layout_constraintTop_toTopOf="@id/help_fragment_emoji_4" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/help_fragment_emoji_2"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/help_fragment_emoji_radio_background"
|
||||
android:button="@null"
|
||||
android:gravity="center"
|
||||
android:scaleType="fitCenter"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/help_fragment_emoji_3"
|
||||
app:layout_constraintEnd_toStartOf="@id/help_fragment_emoji_1"
|
||||
app:layout_constraintStart_toEndOf="@id/help_fragment_emoji_3"
|
||||
app:layout_constraintTop_toTopOf="@id/help_fragment_emoji_3" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/help_fragment_emoji_1"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/help_fragment_emoji_radio_background"
|
||||
android:button="@null"
|
||||
android:gravity="center"
|
||||
android:scaleType="fitCenter"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/help_fragment_emoji_2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/help_fragment_emoji_2"
|
||||
app:layout_constraintTop_toTopOf="@id/help_fragment_emoji_2" />
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/help_fragment_debug"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:checked="true"
|
||||
android:text="@string/HelpFragment__include_debug_log"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/help_fragment_debug_info"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/help_fragment_debug_info" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/help_fragment_debug_info"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:text="@string/HelpFragment__whats_this"
|
||||
android:textAllCaps="false"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/help_fragment_debug"
|
||||
app:layout_constraintTop_toBottomOf="@id/help_fragment_emoji_1" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/help_fragment_faq"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/HelpFragment__have_you_read_our_faq_yet"
|
||||
android:textAllCaps="false"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/help_fragment_next"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
android:id="@+id/help_fragment_next"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:enabled="false"
|
||||
app:circularProgressMaterialButton__label="@string/HelpFragment__next"
|
||||
app:layout_constraintBottom_toBottomOf="@id/help_fragment_faq"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/help_fragment_faq"
|
||||
app:layout_constraintTop_toTopOf="@id/help_fragment_faq" />
|
||||
|
||||
<View
|
||||
android:id="@+id/help_fragment_next_toaster"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:enabled="true"
|
||||
app:layout_constraintBottom_toBottomOf="@id/help_fragment_faq"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/help_fragment_faq"
|
||||
app:layout_constraintTop_toTopOf="@id/help_fragment_faq" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -4101,6 +4101,11 @@
|
||||
<string name="HelpFragment__debug_log" translatable="false">Debug Log:</string>
|
||||
<string name="HelpFragment__could_not_upload_logs">Could not upload logs</string>
|
||||
<string name="HelpFragment__please_be_as_descriptive_as_possible">Please be as descriptive as possible to help us understand the issue.</string>
|
||||
<!-- Error shown under the "tell us what's going on" field when the entered description is shorter than the required minimum length. The placeholder is the minimum number of characters. -->
|
||||
<plurals name="HelpFragment__must_be_at_least_n_characters">
|
||||
<item quantity="one">Must be at least %1$d character</item>
|
||||
<item quantity="other">Must be at least %1$d characters</item>
|
||||
</plurals>
|
||||
<string-array name="HelpFragment__categories_6">
|
||||
<item>Please select an option</item>
|
||||
<item>Something\'s Not Working</item>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user