From 7c3acf78e7a305a6ffbed8fb54a8b3dcf5fbd21c Mon Sep 17 00:00:00 2001 From: ArseniiS Date: Tue, 5 May 2026 03:45:13 +0200 Subject: [PATCH 01/11] Migrate HelpFragment to Compose --- .../settings/app/AppSettingsActivity.kt | 2 +- .../remote/RemoteBackupsSettingsFragment.kt | 4 +- .../errors/DonationErrorDialogs.kt | 2 +- .../errors/DonationErrorNotifications.kt | 2 +- .../manage/ManageDonationsFragment.kt | 2 +- .../securesms/help/refactor/HelpFragment.kt | 46 ++ .../securesms/help/refactor/HelpScreen.kt | 408 ++++++++++++++++++ .../help/refactor/HelpScreenEvents.kt | 11 + .../help/refactor/HelpScreenState.kt | 21 + .../securesms/help/refactor/HelpViewModel.kt | 111 +++++ .../preferences/PaymentsHomeFragment.java | 2 +- .../app_settings_with_change_number.xml | 15 +- 12 files changed, 618 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreen.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenEvents.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenState.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpViewModel.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt index fc6dbc0b587..a572905c891 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt @@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRoute import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayComponent import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayRepository -import org.thoughtcrime.securesms.help.HelpFragment +import org.thoughtcrime.securesms.help.refactor.HelpFragment import org.thoughtcrime.securesms.keyvalue.SettingsValues import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.profiles.manage.UsernameEditMode diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index 4bbe2e0a086..851ab9aef88 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -106,7 +106,7 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.components.settings.app.backups.BackupState import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection -import org.thoughtcrime.securesms.help.HelpFragment +import org.thoughtcrime.securesms.help.refactor.HelpFragment.Companion.REMOTE_BACKUPS_INDEX import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity @@ -234,7 +234,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { override fun onContactSupport() { requireActivity().finish() - requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.REMOTE_BACKUPS_INDEX)) + requireActivity().startActivity(AppSettingsActivity.help(requireContext(), REMOTE_BACKUPS_INDEX)) } override fun onLearnMoreAboutBackupFailure() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt index 77608de8532..b227aee5738 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt @@ -6,7 +6,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.database.InAppPaymentTable -import org.thoughtcrime.securesms.help.HelpFragment +import org.thoughtcrime.securesms.help.refactor.HelpFragment import org.thoughtcrime.securesms.util.CommunicationActions /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt index 30af7ef1f6a..de26f87e061 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt @@ -13,7 +13,7 @@ import org.signal.core.util.PendingIntentFlags import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity -import org.thoughtcrime.securesms.help.HelpFragment +import org.thoughtcrime.securesms.help.refactor.HelpFragment import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.NotificationIds diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt index 10977a56f5b..cffc58bf74f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt @@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation -import org.thoughtcrime.securesms.help.HelpFragment +import org.thoughtcrime.securesms.help.refactor.HelpFragment import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.payments.FiatMoneyUtil diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpFragment.kt new file mode 100644 index 00000000000..d81e1f99d58 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpFragment.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.help.refactor + +import android.content.Intent +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.fragment.app.viewModels +import org.signal.core.ui.compose.ComposeFragment +import org.thoughtcrime.securesms.R + +class HelpFragment : ComposeFragment() { + + private val viewModel: HelpViewModel by viewModels() + + @Composable + override fun FragmentContent() { + val startCategoryIndex = arguments?.getInt(START_CATEGORY_INDEX, 0) ?: 6 + + HelpScreen( + viewModel = viewModel, + startCategoryIndex = startCategoryIndex, + onNavigationClick = { requireActivity().onBackPressedDispatcher.onBackPressed() }, + onWhatIsDebugLogClick = { + val intent = Intent(Intent.ACTION_VIEW) + intent.setData(Uri.parse(getString(R.string.HelpFragment__link__debug_info))) + startActivity(intent) + }, + onFaqClick = { + val intent = Intent(Intent.ACTION_VIEW) + intent.setData(Uri.parse(getString(R.string.HelpFragment__link__faq))) + startActivity(intent) + }, + ) + } + + 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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreen.kt new file mode 100644 index 00000000000..87c07492c3f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreen.kt @@ -0,0 +1,408 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.help.refactor + +import android.widget.ImageView +import androidx.activity.compose.LocalActivity +import androidx.compose.foundation.background +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.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +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.DisposableEffect +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +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.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.WindowCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle +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.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.emoji.EmojiImageView +import org.thoughtcrime.securesms.util.CommunicationActions +import org.thoughtcrime.securesms.util.SupportEmailUtil + +@Composable +fun HelpScreen( + viewModel: HelpViewModel, + startCategoryIndex: Int = 0, + onNavigationClick: () -> Unit, + onWhatIsDebugLogClick: () -> Unit, + onFaqClick: () -> Unit, +) { + val activity = LocalActivity.current + val context = LocalContext.current + val categories = stringArrayResource(R.array.HelpFragment__categories_6).toList() + + val state by viewModel.state.collectAsStateWithLifecycle() + + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(startCategoryIndex) { + viewModel.onCategorySelected(startCategoryIndex) + } + + LaunchedEffect(Unit) { + viewModel.events.collect { event -> + when (event) { + is HelpScreenEvents.OpenEmail -> { + CommunicationActions.openEmail( + context, + SupportEmailUtil.getSupportEmailAddress(context), + event.subject, + event.body, + ) + } + is HelpScreenEvents.ShowSnackbar -> { + snackbarHostState.showSnackbar(context.getString(event.messageRes)) + } + } + } + } + + DisposableEffect(Unit) { + activity?.window?.let { + WindowCompat.setDecorFitsSystemWindows(it, false) + } + onDispose { + activity?.window?.let { + WindowCompat.setDecorFitsSystemWindows(it, true) + } + } + } + + HelpScreenContent( + state = state, + categories = categories, + snackbarHostState = snackbarHostState, + onNavigationClick = { onNavigationClick() }, + onWhatIsDebugLogClick = { onWhatIsDebugLogClick() }, + onFaqClick = { onFaqClick() }, + onProblemTextChanged = viewModel::onProblemChanged, + onCategorySelected = viewModel::onCategorySelected, + onFeelingSelected = viewModel::onFeelingSelected, + onDebugLogsToggled = viewModel::onDebugLogsToggled, + onNextClick = viewModel::onNextClick, + ) +} + +@Composable +private fun HelpScreenContent( + state: HelpScreenState, + categories: List, + snackbarHostState: SnackbarHostState, + onNavigationClick: () -> Unit, + onWhatIsDebugLogClick: () -> Unit, + onFaqClick: () -> Unit, + onProblemTextChanged: (String) -> Unit, + onCategorySelected: (Int) -> Unit, + onFeelingSelected: (Feeling) -> Unit, + onDebugLogsToggled: (Boolean) -> Unit, + onNextClick: () -> Unit, +) { + Scaffolds.Settings( + snackbarHost = { Snackbars.Host(snackbarHostState = snackbarHostState) }, + title = stringResource(R.string.preferences__help), + onNavigationClick = onNavigationClick, + navigationIcon = SignalIcons.ArrowStart.imageVector, + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp), + ) { + Text( + modifier = Modifier.padding(top = 8.dp), + text = stringResource(id = R.string.HelpFragment__contact_us), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + TextField( + value = state.problemText, + onValueChange = { onProblemTextChanged(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), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(id = R.string.HelpFragment__tell_us_why_youre_reaching_out), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + CategoryDropdown( + categories = categories, + selectedIndex = state.categoryIndex, + onCategorySelected = onCategorySelected, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(id = R.string.HelpFragment__how_do_you_feel), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + EmojiRatingRow( + selectedFeeling = state.selectedFeeling, + onFeelingSelected = onFeelingSelected, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = state.includeDebugLog, + onCheckedChange = { onDebugLogsToggled(it) }, + ) + Text( + text = stringResource(id = R.string.HelpFragment__include_debug_log), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + TextButton(onClick = onWhatIsDebugLogClick) { + Text( + text = stringResource(id = R.string.HelpFragment__whats_this), + color = MaterialTheme.colorScheme.primary, + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + } + + 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 = { onFaqClick() }, + 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 = onNextClick, + enabled = !state.isSubmitting, + ) { + Text(stringResource(R.string.HelpFragment__next)) + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CategoryDropdown( + categories: List, + selectedIndex: Int, + onCategorySelected: (Int) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + 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, +) { + 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, + ) { + AndroidView( + factory = { context -> + EmojiImageView(context).apply { + scaleType = ImageView.ScaleType.FIT_CENTER + } + }, + update = { view -> + view.setImageEmoji(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(), + categories = emptyList(), + snackbarHostState = SnackbarHostState(), + onNavigationClick = {}, + onWhatIsDebugLogClick = {}, + onFaqClick = {}, + onProblemTextChanged = {}, + onCategorySelected = {}, + onFeelingSelected = {}, + onDebugLogsToggled = {}, + onNextClick = {}, + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenEvents.kt b/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenEvents.kt new file mode 100644 index 00000000000..d54906830a6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenEvents.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.help.refactor + +sealed interface HelpScreenEvents { + data class OpenEmail(val subject: String, val body: String) : HelpScreenEvents + data class ShowSnackbar(val messageRes: Int) : HelpScreenEvents +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenState.kt b/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenState.kt new file mode 100644 index 00000000000..58a6aef8fb1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenState.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.help.refactor + +data class HelpScreenState( + val problemText: String = "", + val categoryIndex: Int = 0, + val selectedFeeling: Feeling? = null, + val includeDebugLog: Boolean = true, + val isSubmitting: Boolean = false, +) { + val isFormValid: Boolean + get() = problemText.length >= MINIMUM_PROBLEM_CHARS && categoryIndex > 0 + + private companion object { + private const val MINIMUM_PROBLEM_CHARS = 10 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpViewModel.kt new file mode 100644 index 00000000000..d7d174d314e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpViewModel.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.help.refactor + +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(application: Application) : AndroidViewModel(application) { + + private val _state = MutableStateFlow(HelpScreenState()) + val state = _state.asStateFlow() + + private val _events = Channel(Channel.BUFFERED) + val events = _events.receiveAsFlow() + + private val submitDebugLogRepository = SubmitDebugLogRepository() + + fun onProblemChanged(text: String) { + _state.update { it.copy(problemText = text) } + } + + fun onCategorySelected(index: Int) { + _state.update { it.copy(categoryIndex = index) } + } + + fun onFeelingSelected(feeling: Feeling) { + _state.update { current -> + current.copy(selectedFeeling = if (current.selectedFeeling == feeling) null else feeling) + } + } + + fun onDebugLogsToggled(include: Boolean) { + _state.update { it.copy(includeDebugLog = include) } + } + + fun onNextClick() { + if (!state.value.isFormValid) { + viewModelScope.launch { + _events.send(HelpScreenEvents.ShowSnackbar(R.string.HelpFragment__please_be_as_descriptive_as_possible)) + } + return + } + + viewModelScope.launch { + if (_state.value.includeDebugLog) { + _state.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 = _state.value + val englishCategories: Array = 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 { + _events.send(HelpScreenEvents.OpenEmail(subject = subject, body = body)) + _state.update { it.copy(isSubmitting = false) } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java index 130be5dd106..05d94b95933 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java @@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.banner.BannerManager; import org.thoughtcrime.securesms.banner.banners.EnclaveFailureBanner; import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity; -import org.thoughtcrime.securesms.help.HelpFragment; +import org.thoughtcrime.securesms.help.refactor.HelpFragment; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity; import org.thoughtcrime.securesms.payments.FiatMoneyUtil; diff --git a/app/src/main/res/navigation/app_settings_with_change_number.xml b/app/src/main/res/navigation/app_settings_with_change_number.xml index decf073511a..01d63fa59f7 100644 --- a/app/src/main/res/navigation/app_settings_with_change_number.xml +++ b/app/src/main/res/navigation/app_settings_with_change_number.xml @@ -584,9 +584,22 @@ app:popExitAnim="@anim/fragment_close_exit" /> + + + + + + + + + + + + + Date: Tue, 5 May 2026 03:56:59 +0200 Subject: [PATCH 02/11] Clean up code, remove old HelpFragment --- .../settings/app/AppSettingsActivity.kt | 2 +- .../remote/RemoteBackupsSettingsFragment.kt | 2 +- .../errors/DonationErrorDialogs.kt | 2 +- .../errors/DonationErrorNotifications.kt | 2 +- .../manage/ManageDonationsFragment.kt | 2 +- .../securesms/help/HelpFragment.java | 270 ------------------ .../help/{refactor => }/HelpFragment.kt | 2 +- .../help/{refactor => }/HelpScreen.kt | 2 +- .../help/{refactor => }/HelpScreenEvents.kt | 2 +- .../help/{refactor => }/HelpScreenState.kt | 2 +- .../securesms/help/HelpViewModel.java | 79 ----- .../help/{refactor => }/HelpViewModel.kt | 2 +- .../preferences/PaymentsHomeFragment.java | 2 +- .../help_fragment_emoji_radio_background.xml | 13 - app/src/main/res/layout/help_fragment.xml | 261 ----------------- .../app_settings_with_change_number.xml | 15 +- 16 files changed, 12 insertions(+), 648 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java rename app/src/main/java/org/thoughtcrime/securesms/help/{refactor => }/HelpFragment.kt (96%) rename app/src/main/java/org/thoughtcrime/securesms/help/{refactor => }/HelpScreen.kt (99%) rename app/src/main/java/org/thoughtcrime/securesms/help/{refactor => }/HelpScreenEvents.kt (84%) rename app/src/main/java/org/thoughtcrime/securesms/help/{refactor => }/HelpScreenState.kt (90%) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java rename app/src/main/java/org/thoughtcrime/securesms/help/{refactor => }/HelpViewModel.kt (98%) delete mode 100644 app/src/main/res/drawable/help_fragment_emoji_radio_background.xml delete mode 100644 app/src/main/res/layout/help_fragment.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt index a572905c891..fc6dbc0b587 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt @@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRoute import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayComponent import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayRepository -import org.thoughtcrime.securesms.help.refactor.HelpFragment +import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.keyvalue.SettingsValues import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.profiles.manage.UsernameEditMode diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index 851ab9aef88..e11ed0f8c83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -106,7 +106,7 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.components.settings.app.backups.BackupState import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection -import org.thoughtcrime.securesms.help.refactor.HelpFragment.Companion.REMOTE_BACKUPS_INDEX +import org.thoughtcrime.securesms.help.HelpFragment.Companion.REMOTE_BACKUPS_INDEX import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt index b227aee5738..77608de8532 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt @@ -6,7 +6,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.database.InAppPaymentTable -import org.thoughtcrime.securesms.help.refactor.HelpFragment +import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.util.CommunicationActions /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt index de26f87e061..30af7ef1f6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt @@ -13,7 +13,7 @@ import org.signal.core.util.PendingIntentFlags import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity -import org.thoughtcrime.securesms.help.refactor.HelpFragment +import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.NotificationIds diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt index cffc58bf74f..10977a56f5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt @@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation -import org.thoughtcrime.securesms.help.refactor.HelpFragment +import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.payments.FiatMoneyUtil diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java deleted file mode 100644 index a9090bbb8e3..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java +++ /dev/null @@ -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 emoji; - private HelpViewModel helpViewModel; - private Spinner categorySpinner; - private ArrayAdapter 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(); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpFragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt index d81e1f99d58..36d3d7cef0b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.help.refactor +package org.thoughtcrime.securesms.help import android.content.Intent import android.net.Uri diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt similarity index 99% rename from app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreen.kt rename to app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt index 87c07492c3f..043e3d1ab0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.help.refactor +package org.thoughtcrime.securesms.help import android.widget.ImageView import androidx.activity.compose.LocalActivity diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenEvents.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt similarity index 84% rename from app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenEvents.kt rename to app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt index d54906830a6..c4536a76fa1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenEvents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.help.refactor +package org.thoughtcrime.securesms.help sealed interface HelpScreenEvents { data class OpenEmail(val subject: String, val body: String) : HelpScreenEvents diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenState.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenState.kt similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenState.kt rename to app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenState.kt index 58a6aef8fb1..29105484c9b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpScreenState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenState.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.help.refactor +package org.thoughtcrime.securesms.help data class HelpScreenState( val problemText: String = "", diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java deleted file mode 100644 index 34f01fb0c3c..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java +++ /dev/null @@ -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 problemMeetsLengthRequirements; - private final MutableLiveData categoryIndex; - private final LiveData 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 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 onSubmitClicked(boolean includeDebugLogs) { - MutableLiveData 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 debugLogUrl; - private final boolean isError; - - private SubmitResult(@NonNull Optional debugLogUrl, boolean isError) { - this.debugLogUrl = debugLogUrl; - this.isError = isError; - } - - @NonNull Optional getDebugLogUrl() { - return debugLogUrl; - } - - boolean isError() { - return isError; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpViewModel.kt rename to app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt index d7d174d314e..b61ae1d90e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/refactor/HelpViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.help.refactor +package org.thoughtcrime.securesms.help import android.app.Application import androidx.lifecycle.AndroidViewModel diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java index 05d94b95933..130be5dd106 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java @@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.banner.BannerManager; import org.thoughtcrime.securesms.banner.banners.EnclaveFailureBanner; import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity; -import org.thoughtcrime.securesms.help.refactor.HelpFragment; +import org.thoughtcrime.securesms.help.HelpFragment; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity; import org.thoughtcrime.securesms.payments.FiatMoneyUtil; diff --git a/app/src/main/res/drawable/help_fragment_emoji_radio_background.xml b/app/src/main/res/drawable/help_fragment_emoji_radio_background.xml deleted file mode 100644 index efc885f3587..00000000000 --- a/app/src/main/res/drawable/help_fragment_emoji_radio_background.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/help_fragment.xml b/app/src/main/res/layout/help_fragment.xml deleted file mode 100644 index 0f28f0aedc4..00000000000 --- a/app/src/main/res/layout/help_fragment.xml +++ /dev/null @@ -1,261 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/app_settings_with_change_number.xml b/app/src/main/res/navigation/app_settings_with_change_number.xml index 01d63fa59f7..decf073511a 100644 --- a/app/src/main/res/navigation/app_settings_with_change_number.xml +++ b/app/src/main/res/navigation/app_settings_with_change_number.xml @@ -584,22 +584,9 @@ app:popExitAnim="@anim/fragment_close_exit" /> - - - - - - - - - - - - - Date: Thu, 7 May 2026 17:19:52 +0200 Subject: [PATCH 03/11] Resolve PR comments --- .../remote/RemoteBackupsSettingsFragment.kt | 4 ++-- .../securesms/help/HelpFragment.kt | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index e11ed0f8c83..4bbe2e0a086 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -106,7 +106,7 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.components.settings.app.backups.BackupState import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection -import org.thoughtcrime.securesms.help.HelpFragment.Companion.REMOTE_BACKUPS_INDEX +import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity @@ -234,7 +234,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { override fun onContactSupport() { requireActivity().finish() - requireActivity().startActivity(AppSettingsActivity.help(requireContext(), REMOTE_BACKUPS_INDEX)) + requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.REMOTE_BACKUPS_INDEX)) } override fun onLearnMoreAboutBackupFailure() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt index 36d3d7cef0b..ca70b02842c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt @@ -5,12 +5,11 @@ package org.thoughtcrime.securesms.help -import android.content.Intent -import android.net.Uri import androidx.compose.runtime.Composable import androidx.fragment.app.viewModels import org.signal.core.ui.compose.ComposeFragment import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.util.CommunicationActions class HelpFragment : ComposeFragment() { @@ -18,21 +17,23 @@ class HelpFragment : ComposeFragment() { @Composable override fun FragmentContent() { - val startCategoryIndex = arguments?.getInt(START_CATEGORY_INDEX, 0) ?: 6 + val startCategoryIndex = arguments?.getInt(START_CATEGORY_INDEX, 0) ?: PAYMENT_INDEX HelpScreen( viewModel = viewModel, startCategoryIndex = startCategoryIndex, onNavigationClick = { requireActivity().onBackPressedDispatcher.onBackPressed() }, onWhatIsDebugLogClick = { - val intent = Intent(Intent.ACTION_VIEW) - intent.setData(Uri.parse(getString(R.string.HelpFragment__link__debug_info))) - startActivity(intent) + CommunicationActions.openBrowserLink( + requireContext(), + getString(R.string.HelpFragment__link__debug_info) + ) }, onFaqClick = { - val intent = Intent(Intent.ACTION_VIEW) - intent.setData(Uri.parse(getString(R.string.HelpFragment__link__faq))) - startActivity(intent) + CommunicationActions.openBrowserLink( + requireContext(), + getString(R.string.HelpFragment__link__faq) + ) }, ) } From d21eb8cdcaab5c2b510a2b821ed7c2b419b54859 Mon Sep 17 00:00:00 2001 From: ArseniiS Date: Thu, 7 May 2026 17:23:19 +0200 Subject: [PATCH 04/11] Fix internal state & events variable names --- .../securesms/help/HelpViewModel.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt index b61ae1d90e7..ecde5d72e8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt @@ -22,43 +22,43 @@ import org.thoughtcrime.securesms.util.SupportEmailUtil class HelpViewModel(application: Application) : AndroidViewModel(application) { - private val _state = MutableStateFlow(HelpScreenState()) - val state = _state.asStateFlow() + private val internalState = MutableStateFlow(HelpScreenState()) + val state = internalState.asStateFlow() - private val _events = Channel(Channel.BUFFERED) - val events = _events.receiveAsFlow() + private val internalEvents = Channel(Channel.BUFFERED) + val events = internalEvents.receiveAsFlow() private val submitDebugLogRepository = SubmitDebugLogRepository() fun onProblemChanged(text: String) { - _state.update { it.copy(problemText = text) } + internalState.update { it.copy(problemText = text) } } fun onCategorySelected(index: Int) { - _state.update { it.copy(categoryIndex = index) } + internalState.update { it.copy(categoryIndex = index) } } fun onFeelingSelected(feeling: Feeling) { - _state.update { current -> + internalState.update { current -> current.copy(selectedFeeling = if (current.selectedFeeling == feeling) null else feeling) } } fun onDebugLogsToggled(include: Boolean) { - _state.update { it.copy(includeDebugLog = include) } + internalState.update { it.copy(includeDebugLog = include) } } fun onNextClick() { if (!state.value.isFormValid) { viewModelScope.launch { - _events.send(HelpScreenEvents.ShowSnackbar(R.string.HelpFragment__please_be_as_descriptive_as_possible)) + internalEvents.send(HelpScreenEvents.ShowSnackbar(R.string.HelpFragment__please_be_as_descriptive_as_possible)) } return } viewModelScope.launch { - if (_state.value.includeDebugLog) { - _state.update { it.copy(isSubmitting = true) } + if (internalState.value.includeDebugLog) { + internalState.update { it.copy(isSubmitting = true) } submitDebugLogRepository.buildAndSubmitLog { optionalUrl -> val debugLogUrl = if (optionalUrl.isPresent) optionalUrl.get() @@ -74,7 +74,7 @@ class HelpViewModel(application: Application) : AndroidViewModel(application) { private fun dispatchEmail(debugLogUrl: String?) { val context = application - val state = _state.value + val state = internalState.value val englishCategories: Array = ResourceUtil.getEnglishResources(context) .getStringArray(R.array.HelpFragment__categories_6) val categoryLabel = englishCategories.getOrElse(state.categoryIndex) { "" } @@ -104,8 +104,8 @@ class HelpViewModel(application: Application) : AndroidViewModel(application) { ) viewModelScope.launch { - _events.send(HelpScreenEvents.OpenEmail(subject = subject, body = body)) - _state.update { it.copy(isSubmitting = false) } + internalEvents.send(HelpScreenEvents.OpenEmail(subject = subject, body = body)) + internalState.update { it.copy(isSubmitting = false) } } } } From f419d2c1da34c159a4889f29b397e0b03026201e Mon Sep 17 00:00:00 2001 From: ArseniiS Date: Thu, 7 May 2026 17:37:27 +0200 Subject: [PATCH 05/11] Fix code formatting --- .../securesms/help/HelpFragment.kt | 6 +- .../thoughtcrime/securesms/help/HelpScreen.kt | 72 +++++++++---------- .../securesms/help/HelpScreenState.kt | 2 +- .../securesms/help/HelpViewModel.kt | 6 +- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt index ca70b02842c..daa11606db9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt @@ -34,14 +34,14 @@ class HelpFragment : ComposeFragment() { requireContext(), getString(R.string.HelpFragment__link__faq) ) - }, + } ) } companion object { const val START_CATEGORY_INDEX = "start_category_index" - const val PAYMENT_INDEX = 6 - const val DONATION_INDEX = 7 + const val PAYMENT_INDEX = 6 + const val DONATION_INDEX = 7 const val REMOTE_BACKUPS_INDEX = 8 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt index 043e3d1ab0e..2e1ce68058f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt @@ -78,7 +78,7 @@ fun HelpScreen( startCategoryIndex: Int = 0, onNavigationClick: () -> Unit, onWhatIsDebugLogClick: () -> Unit, - onFaqClick: () -> Unit, + onFaqClick: () -> Unit ) { val activity = LocalActivity.current val context = LocalContext.current @@ -100,7 +100,7 @@ fun HelpScreen( context, SupportEmailUtil.getSupportEmailAddress(context), event.subject, - event.body, + event.body ) } is HelpScreenEvents.ShowSnackbar -> { @@ -132,7 +132,7 @@ fun HelpScreen( onCategorySelected = viewModel::onCategorySelected, onFeelingSelected = viewModel::onFeelingSelected, onDebugLogsToggled = viewModel::onDebugLogsToggled, - onNextClick = viewModel::onNextClick, + onNextClick = viewModel::onNextClick ) } @@ -148,30 +148,30 @@ private fun HelpScreenContent( onCategorySelected: (Int) -> Unit, onFeelingSelected: (Feeling) -> Unit, onDebugLogsToggled: (Boolean) -> Unit, - onNextClick: () -> Unit, + onNextClick: () -> Unit ) { Scaffolds.Settings( snackbarHost = { Snackbars.Host(snackbarHostState = snackbarHostState) }, title = stringResource(R.string.preferences__help), onNavigationClick = onNavigationClick, - navigationIcon = SignalIcons.ArrowStart.imageVector, + navigationIcon = SignalIcons.ArrowStart.imageVector ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() - .padding(paddingValues), + .padding(paddingValues) ) { Column( modifier = Modifier .weight(1f) .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp), + .padding(horizontal = 16.dp) ) { Text( modifier = Modifier.padding(top = 8.dp), text = stringResource(id = R.string.HelpFragment__contact_us), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(12.dp)) @@ -184,12 +184,12 @@ private fun HelpScreenContent( }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, - capitalization = KeyboardCapitalization.Sentences, + capitalization = KeyboardCapitalization.Sentences ), maxLines = Int.MAX_VALUE, modifier = Modifier .fillMaxWidth() - .defaultMinSize(minHeight = 144.dp), + .defaultMinSize(minHeight = 144.dp) ) Spacer(modifier = Modifier.height(16.dp)) @@ -197,7 +197,7 @@ private fun HelpScreenContent( Text( text = stringResource(id = R.string.HelpFragment__tell_us_why_youre_reaching_out), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(8.dp)) @@ -205,7 +205,7 @@ private fun HelpScreenContent( CategoryDropdown( categories = categories, selectedIndex = state.categoryIndex, - onCategorySelected = onCategorySelected, + onCategorySelected = onCategorySelected ) Spacer(modifier = Modifier.height(16.dp)) @@ -213,35 +213,35 @@ private fun HelpScreenContent( Text( text = stringResource(id = R.string.HelpFragment__how_do_you_feel), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(12.dp)) EmojiRatingRow( selectedFeeling = state.selectedFeeling, - onFeelingSelected = onFeelingSelected, + onFeelingSelected = onFeelingSelected ) Spacer(modifier = Modifier.height(12.dp)) Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { Checkbox( checked = state.includeDebugLog, - onCheckedChange = { onDebugLogsToggled(it) }, + onCheckedChange = { onDebugLogsToggled(it) } ) Text( text = stringResource(id = R.string.HelpFragment__include_debug_log), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = MaterialTheme.colorScheme.onSurfaceVariant ) TextButton(onClick = onWhatIsDebugLogClick) { Text( text = stringResource(id = R.string.HelpFragment__whats_this), - color = MaterialTheme.colorScheme.primary, + color = MaterialTheme.colorScheme.primary ) } } @@ -253,7 +253,7 @@ private fun HelpScreenContent( modifier = Modifier .fillMaxWidth() .padding(bottom = 16.dp, start = 16.dp), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { Text( modifier = Modifier @@ -268,16 +268,16 @@ private fun HelpScreenContent( ) { append(stringResource(R.string.HelpFragment__have_you_read_our_faq_yet)) } - }, + } ) CircularProgressWrapper( - isLoading = state.isSubmitting, + isLoading = state.isSubmitting ) { Buttons.LargeTonal( modifier = Modifier.padding(end = 16.dp), onClick = onNextClick, - enabled = !state.isSubmitting, + enabled = !state.isSubmitting ) { Text(stringResource(R.string.HelpFragment__next)) } @@ -292,24 +292,24 @@ private fun HelpScreenContent( private fun CategoryDropdown( categories: List, selectedIndex: Int, - onCategorySelected: (Int) -> Unit, + onCategorySelected: (Int) -> Unit ) { var expanded by remember { mutableStateOf(false) } ExposedDropdownMenuBox( expanded = expanded, - onExpandedChange = { expanded = !expanded }, + onExpandedChange = { expanded = !expanded } ) { TextField( modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, true), value = categories.getOrElse(selectedIndex) { "" }, onValueChange = {}, readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) } ) ExposedDropdownMenu( expanded = expanded, - onDismissRequest = { expanded = false }, + onDismissRequest = { expanded = false } ) { categories.forEachIndexed { index, category -> DropdownMenuItem( @@ -317,7 +317,7 @@ private fun CategoryDropdown( onClick = { onCategorySelected(index) expanded = false - }, + } ) } } @@ -327,17 +327,17 @@ private fun CategoryDropdown( @Composable private fun EmojiRatingRow( selectedFeeling: Feeling?, - onFeelingSelected: (Feeling) -> Unit, + onFeelingSelected: (Feeling) -> Unit ) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Feeling.entries.forEach { feeling -> EmojiButton( feeling = feeling, isSelected = feeling == selectedFeeling, - onClick = { onFeelingSelected(feeling) }, + onClick = { onFeelingSelected(feeling) } ) } } @@ -347,7 +347,7 @@ private fun EmojiRatingRow( private fun EmojiButton( feeling: Feeling, isSelected: Boolean, - onClick: () -> Unit, + onClick: () -> Unit ) { val isDark = isSystemInDarkTheme() @@ -363,7 +363,7 @@ private fun EmojiButton( .background(backgroundColor, shape = CircleShape) .padding(4.dp) .clickable(onClick = onClick), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { AndroidView( factory = { context -> @@ -381,10 +381,10 @@ private fun EmojiButton( 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), + 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), + UNHAPPY(emojiCode = "\ud83d\ude41", labelRes = R.string.HelpFragment__emoji_2), + ANGRY(emojiCode = "\ud83d\ude20", labelRes = R.string.HelpFragment__emoji_1) } @DayNightPreviews @@ -402,7 +402,7 @@ private fun HelpScreenPreview() { onCategorySelected = {}, onFeelingSelected = {}, onDebugLogsToggled = {}, - onNextClick = {}, + onNextClick = {} ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenState.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenState.kt index 29105484c9b..d368aef54c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenState.kt @@ -10,7 +10,7 @@ data class HelpScreenState( val categoryIndex: Int = 0, val selectedFeeling: Feeling? = null, val includeDebugLog: Boolean = true, - val isSubmitting: Boolean = false, + val isSubmitting: Boolean = false ) { val isFormValid: Boolean get() = problemText.length >= MINIMUM_PROBLEM_CHARS && categoryIndex > 0 diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt index ecde5d72e8d..93fab117fb4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt @@ -76,7 +76,7 @@ class HelpViewModel(application: Application) : AndroidViewModel(application) { val context = application val state = internalState.value val englishCategories: Array = ResourceUtil.getEnglishResources(context) - .getStringArray(R.array.HelpFragment__categories_6) + .getStringArray(R.array.HelpFragment__categories_6) val categoryLabel = englishCategories.getOrElse(state.categoryIndex) { "" } val suffix = buildString { @@ -95,12 +95,12 @@ class HelpViewModel(application: Application) : AndroidViewModel(application) { } val subject = context.getString(R.string.HelpFragment__signal_android_support_request) - val body = SupportEmailUtil.generateSupportEmailBody( + val body = SupportEmailUtil.generateSupportEmailBody( context, R.string.HelpFragment__signal_android_support_request, " - $categoryLabel", "${state.problemText}\n\n", - suffix, + suffix ) viewModelScope.launch { From 28193395235653515bf490b2a62c1dd587b149fb Mon Sep 17 00:00:00 2001 From: ArseniiS Date: Fri, 8 May 2026 20:02:05 +0200 Subject: [PATCH 06/11] Rename HelpScreenEvents class to HelpScreenSideEffects class, keep Events class for user interaction --- .../java/org/thoughtcrime/securesms/help/HelpScreen.kt | 6 +++--- .../help/{HelpScreenEvents.kt => HelpScreenSideEffect.kt} | 6 +++--- .../java/org/thoughtcrime/securesms/help/HelpViewModel.kt | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/help/{HelpScreenEvents.kt => HelpScreenSideEffect.kt} (60%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt index 2e1ce68058f..761c00c0b33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt @@ -93,9 +93,9 @@ fun HelpScreen( } LaunchedEffect(Unit) { - viewModel.events.collect { event -> + viewModel.sideEffect.collect { event -> when (event) { - is HelpScreenEvents.OpenEmail -> { + is HelpScreenSideEffect.OpenEmail -> { CommunicationActions.openEmail( context, SupportEmailUtil.getSupportEmailAddress(context), @@ -103,7 +103,7 @@ fun HelpScreen( event.body ) } - is HelpScreenEvents.ShowSnackbar -> { + is HelpScreenSideEffect.ShowSnackbar -> { snackbarHostState.showSnackbar(context.getString(event.messageRes)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenSideEffect.kt similarity index 60% rename from app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt rename to app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenSideEffect.kt index c4536a76fa1..6266f022062 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenSideEffect.kt @@ -5,7 +5,7 @@ package org.thoughtcrime.securesms.help -sealed interface HelpScreenEvents { - data class OpenEmail(val subject: String, val body: String) : HelpScreenEvents - data class ShowSnackbar(val messageRes: Int) : HelpScreenEvents +sealed interface HelpScreenSideEffect { + data class OpenEmail(val subject: String, val body: String) : HelpScreenSideEffect + data class ShowSnackbar(val messageRes: Int) : HelpScreenSideEffect } diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt index 93fab117fb4..8ee98917396 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt @@ -25,8 +25,8 @@ class HelpViewModel(application: Application) : AndroidViewModel(application) { private val internalState = MutableStateFlow(HelpScreenState()) val state = internalState.asStateFlow() - private val internalEvents = Channel(Channel.BUFFERED) - val events = internalEvents.receiveAsFlow() + private val internalSideEffect = Channel(Channel.BUFFERED) + val sideEffect = internalSideEffect.receiveAsFlow() private val submitDebugLogRepository = SubmitDebugLogRepository() @@ -51,7 +51,7 @@ class HelpViewModel(application: Application) : AndroidViewModel(application) { fun onNextClick() { if (!state.value.isFormValid) { viewModelScope.launch { - internalEvents.send(HelpScreenEvents.ShowSnackbar(R.string.HelpFragment__please_be_as_descriptive_as_possible)) + internalSideEffect.send(HelpScreenSideEffect.ShowSnackbar(R.string.HelpFragment__please_be_as_descriptive_as_possible)) } return } @@ -104,7 +104,7 @@ class HelpViewModel(application: Application) : AndroidViewModel(application) { ) viewModelScope.launch { - internalEvents.send(HelpScreenEvents.OpenEmail(subject = subject, body = body)) + internalSideEffect.send(HelpScreenSideEffect.OpenEmail(subject = subject, body = body)) internalState.update { it.copy(isSubmitting = false) } } } From 8ca305203dc8996abe27b887a4c37e0a385cea20 Mon Sep 17 00:00:00 2001 From: ArseniiS Date: Fri, 8 May 2026 20:29:27 +0200 Subject: [PATCH 07/11] Pass startCategoryIndex argument directly to VM constructor --- .../org/thoughtcrime/securesms/help/HelpFragment.kt | 13 ++++++++----- .../org/thoughtcrime/securesms/help/HelpScreen.kt | 5 ----- .../thoughtcrime/securesms/help/HelpViewModel.kt | 11 ++++++++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt index daa11606db9..36cafbec371 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt @@ -6,22 +6,25 @@ package org.thoughtcrime.securesms.help import androidx.compose.runtime.Composable -import androidx.fragment.app.viewModels 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 viewModels() + private val viewModel: HelpViewModel by viewModel { + HelpViewModel( + startCategoryIndex = arguments?.getInt(START_CATEGORY_INDEX, 0) ?: PAYMENT_INDEX, + application = AppDependencies.application + ) + } @Composable override fun FragmentContent() { - val startCategoryIndex = arguments?.getInt(START_CATEGORY_INDEX, 0) ?: PAYMENT_INDEX - HelpScreen( viewModel = viewModel, - startCategoryIndex = startCategoryIndex, onNavigationClick = { requireActivity().onBackPressedDispatcher.onBackPressed() }, onWhatIsDebugLogClick = { CommunicationActions.openBrowserLink( diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt index 761c00c0b33..c677b0ba5b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt @@ -75,7 +75,6 @@ import org.thoughtcrime.securesms.util.SupportEmailUtil @Composable fun HelpScreen( viewModel: HelpViewModel, - startCategoryIndex: Int = 0, onNavigationClick: () -> Unit, onWhatIsDebugLogClick: () -> Unit, onFaqClick: () -> Unit @@ -88,10 +87,6 @@ fun HelpScreen( val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(startCategoryIndex) { - viewModel.onCategorySelected(startCategoryIndex) - } - LaunchedEffect(Unit) { viewModel.sideEffect.collect { event -> when (event) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt index 8ee98917396..42af5b7db87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt @@ -20,9 +20,14 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository import org.thoughtcrime.securesms.util.SupportEmailUtil -class HelpViewModel(application: Application) : AndroidViewModel(application) { - - private val internalState = MutableStateFlow(HelpScreenState()) +class HelpViewModel( + startCategoryIndex: Int, + application: Application +) : AndroidViewModel(application) { + + private val internalState = MutableStateFlow(HelpScreenState( + categoryIndex = startCategoryIndex + )) val state = internalState.asStateFlow() private val internalSideEffect = Channel(Channel.BUFFERED) From 72189f7080c0bf8116e84b9b277efc78cf87c952 Mon Sep 17 00:00:00 2001 From: ArseniiS Date: Mon, 11 May 2026 14:09:20 +0200 Subject: [PATCH 08/11] Refactor help screen events --- .../securesms/help/HelpFragment.kt | 10 ++- .../thoughtcrime/securesms/help/HelpScreen.kt | 71 +++++-------------- .../securesms/help/HelpScreenEvents.kt | 14 ++++ .../securesms/help/HelpViewModel.kt | 20 ++++-- 4 files changed, 56 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt index 36cafbec371..dab3b50a250 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.kt @@ -6,6 +6,8 @@ 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 @@ -23,8 +25,12 @@ class HelpFragment : ComposeFragment() { @Composable override fun FragmentContent() { - HelpScreen( - viewModel = viewModel, + val state by viewModel.state.collectAsStateWithLifecycle() + + HelpScreenContent( + state = state, + onEvent = viewModel::onEvent, + sideEffect = viewModel.sideEffect, onNavigationClick = { requireActivity().onBackPressedDispatcher.onBackPressed() }, onWhatIsDebugLogClick = { CommunicationActions.openBrowserLink( diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt index c677b0ba5b0..3702fdd56b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt @@ -59,7 +59,8 @@ import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.WindowCompat -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.CircularProgressWrapper import org.signal.core.ui.compose.DayNightPreviews @@ -73,33 +74,33 @@ import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.SupportEmailUtil @Composable -fun HelpScreen( - viewModel: HelpViewModel, +fun HelpScreenContent( + state: HelpScreenState, + onEvent: (HelpScreenEvents) -> Unit, + sideEffect: Flow, onNavigationClick: () -> Unit, onWhatIsDebugLogClick: () -> Unit, - onFaqClick: () -> Unit + onFaqClick: () -> Unit, ) { val activity = LocalActivity.current val context = LocalContext.current val categories = stringArrayResource(R.array.HelpFragment__categories_6).toList() - val state by viewModel.state.collectAsStateWithLifecycle() - val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(Unit) { - viewModel.sideEffect.collect { event -> - when (event) { + sideEffect.collect { sideEffect -> + when (sideEffect) { is HelpScreenSideEffect.OpenEmail -> { CommunicationActions.openEmail( context, SupportEmailUtil.getSupportEmailAddress(context), - event.subject, - event.body + sideEffect.subject, + sideEffect.body ) } is HelpScreenSideEffect.ShowSnackbar -> { - snackbarHostState.showSnackbar(context.getString(event.messageRes)) + snackbarHostState.showSnackbar(context.getString(sideEffect.messageRes)) } } } @@ -116,35 +117,6 @@ fun HelpScreen( } } - HelpScreenContent( - state = state, - categories = categories, - snackbarHostState = snackbarHostState, - onNavigationClick = { onNavigationClick() }, - onWhatIsDebugLogClick = { onWhatIsDebugLogClick() }, - onFaqClick = { onFaqClick() }, - onProblemTextChanged = viewModel::onProblemChanged, - onCategorySelected = viewModel::onCategorySelected, - onFeelingSelected = viewModel::onFeelingSelected, - onDebugLogsToggled = viewModel::onDebugLogsToggled, - onNextClick = viewModel::onNextClick - ) -} - -@Composable -private fun HelpScreenContent( - state: HelpScreenState, - categories: List, - snackbarHostState: SnackbarHostState, - onNavigationClick: () -> Unit, - onWhatIsDebugLogClick: () -> Unit, - onFaqClick: () -> Unit, - onProblemTextChanged: (String) -> Unit, - onCategorySelected: (Int) -> Unit, - onFeelingSelected: (Feeling) -> Unit, - onDebugLogsToggled: (Boolean) -> Unit, - onNextClick: () -> Unit -) { Scaffolds.Settings( snackbarHost = { Snackbars.Host(snackbarHostState = snackbarHostState) }, title = stringResource(R.string.preferences__help), @@ -173,7 +145,7 @@ private fun HelpScreenContent( TextField( value = state.problemText, - onValueChange = { onProblemTextChanged(it) }, + onValueChange = { onEvent(HelpScreenEvents.ProblemTextChanged(it)) }, placeholder = { Text(text = stringResource(id = R.string.HelpFragment__tell_us_whats_going_on)) }, @@ -200,7 +172,7 @@ private fun HelpScreenContent( CategoryDropdown( categories = categories, selectedIndex = state.categoryIndex, - onCategorySelected = onCategorySelected + onCategorySelected = { onEvent(HelpScreenEvents.CategorySelected(it)) } ) Spacer(modifier = Modifier.height(16.dp)) @@ -215,7 +187,7 @@ private fun HelpScreenContent( EmojiRatingRow( selectedFeeling = state.selectedFeeling, - onFeelingSelected = onFeelingSelected + onFeelingSelected = { onEvent(HelpScreenEvents.FeelingSelected(it)) } ) Spacer(modifier = Modifier.height(12.dp)) @@ -226,7 +198,7 @@ private fun HelpScreenContent( ) { Checkbox( checked = state.includeDebugLog, - onCheckedChange = { onDebugLogsToggled(it) } + onCheckedChange = { onEvent(HelpScreenEvents.DebugLogsToggled(it)) } ) Text( text = stringResource(id = R.string.HelpFragment__include_debug_log), @@ -271,7 +243,7 @@ private fun HelpScreenContent( ) { Buttons.LargeTonal( modifier = Modifier.padding(end = 16.dp), - onClick = onNextClick, + onClick = { onEvent(HelpScreenEvents.OnNextClick) }, enabled = !state.isSubmitting ) { Text(stringResource(R.string.HelpFragment__next)) @@ -388,16 +360,11 @@ private fun HelpScreenPreview() { Previews.Preview { HelpScreenContent( state = HelpScreenState(), - categories = emptyList(), - snackbarHostState = SnackbarHostState(), + onEvent = {}, + sideEffect = emptyFlow(), onNavigationClick = {}, onWhatIsDebugLogClick = {}, onFaqClick = {}, - onProblemTextChanged = {}, - onCategorySelected = {}, - onFeelingSelected = {}, - onDebugLogsToggled = {}, - onNextClick = {} ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt new file mode 100644 index 00000000000..35388989494 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt @@ -0,0 +1,14 @@ +/* + * 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 + object OnNextClick : HelpScreenEvents +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt index 42af5b7db87..76a487210e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt @@ -35,25 +35,35 @@ class HelpViewModel( private val submitDebugLogRepository = SubmitDebugLogRepository() - fun onProblemChanged(text: String) { + 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() + } + } + + private fun onProblemChanged(text: String) { internalState.update { it.copy(problemText = text) } } - fun onCategorySelected(index: Int) { + private fun onCategorySelected(index: Int) { internalState.update { it.copy(categoryIndex = index) } } - fun onFeelingSelected(feeling: Feeling) { + private fun onFeelingSelected(feeling: Feeling) { internalState.update { current -> current.copy(selectedFeeling = if (current.selectedFeeling == feeling) null else feeling) } } - fun onDebugLogsToggled(include: Boolean) { + private fun onDebugLogsToggled(include: Boolean) { internalState.update { it.copy(includeDebugLog = include) } } - fun onNextClick() { + private fun onNextClick() { if (!state.value.isFormValid) { viewModelScope.launch { internalSideEffect.send(HelpScreenSideEffect.ShowSnackbar(R.string.HelpFragment__please_be_as_descriptive_as_possible)) From a7bb041b67664fcf33a8dfe1c66ff93938dfa0b9 Mon Sep 17 00:00:00 2001 From: ArseniiS Date: Mon, 11 May 2026 14:29:04 +0200 Subject: [PATCH 09/11] Fix HelpScreenSideEffects name --- .../main/java/org/thoughtcrime/securesms/help/HelpScreen.kt | 6 +++--- .../{HelpScreenSideEffect.kt => HelpScreenSideEffects.kt} | 6 +++--- .../java/org/thoughtcrime/securesms/help/HelpViewModel.kt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/help/{HelpScreenSideEffect.kt => HelpScreenSideEffects.kt} (78%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt index 3702fdd56b6..327f52eafe7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt @@ -77,7 +77,7 @@ import org.thoughtcrime.securesms.util.SupportEmailUtil fun HelpScreenContent( state: HelpScreenState, onEvent: (HelpScreenEvents) -> Unit, - sideEffect: Flow, + sideEffect: Flow, onNavigationClick: () -> Unit, onWhatIsDebugLogClick: () -> Unit, onFaqClick: () -> Unit, @@ -91,7 +91,7 @@ fun HelpScreenContent( LaunchedEffect(Unit) { sideEffect.collect { sideEffect -> when (sideEffect) { - is HelpScreenSideEffect.OpenEmail -> { + is HelpScreenSideEffects.OpenEmail -> { CommunicationActions.openEmail( context, SupportEmailUtil.getSupportEmailAddress(context), @@ -99,7 +99,7 @@ fun HelpScreenContent( sideEffect.body ) } - is HelpScreenSideEffect.ShowSnackbar -> { + is HelpScreenSideEffects.ShowSnackbar -> { snackbarHostState.showSnackbar(context.getString(sideEffect.messageRes)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenSideEffect.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenSideEffects.kt similarity index 78% rename from app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenSideEffect.kt rename to app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenSideEffects.kt index 6266f022062..e4373a040aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenSideEffect.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenSideEffects.kt @@ -5,7 +5,7 @@ package org.thoughtcrime.securesms.help -sealed interface HelpScreenSideEffect { - data class OpenEmail(val subject: String, val body: String) : HelpScreenSideEffect - data class ShowSnackbar(val messageRes: Int) : HelpScreenSideEffect +sealed interface HelpScreenSideEffects { + data class OpenEmail(val subject: String, val body: String) : HelpScreenSideEffects + data class ShowSnackbar(val messageRes: Int) : HelpScreenSideEffects } diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt index 76a487210e8..c784f085ffb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt @@ -30,7 +30,7 @@ class HelpViewModel( )) val state = internalState.asStateFlow() - private val internalSideEffect = Channel(Channel.BUFFERED) + private val internalSideEffect = Channel(Channel.BUFFERED) val sideEffect = internalSideEffect.receiveAsFlow() private val submitDebugLogRepository = SubmitDebugLogRepository() @@ -66,7 +66,7 @@ class HelpViewModel( private fun onNextClick() { if (!state.value.isFormValid) { viewModelScope.launch { - internalSideEffect.send(HelpScreenSideEffect.ShowSnackbar(R.string.HelpFragment__please_be_as_descriptive_as_possible)) + internalSideEffect.send(HelpScreenSideEffects.ShowSnackbar(R.string.HelpFragment__please_be_as_descriptive_as_possible)) } return } @@ -119,7 +119,7 @@ class HelpViewModel( ) viewModelScope.launch { - internalSideEffect.send(HelpScreenSideEffect.OpenEmail(subject = subject, body = body)) + internalSideEffect.send(HelpScreenSideEffects.OpenEmail(subject = subject, body = body)) internalState.update { it.copy(isSubmitting = false) } } } From f7b81e9eb96722188cfc9b3fe364f8ecea776313 Mon Sep 17 00:00:00 2001 From: ArseniiS Date: Wed, 13 May 2026 02:59:36 +0200 Subject: [PATCH 10/11] Clean up code --- .../thoughtcrime/securesms/help/HelpScreen.kt | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt index 327f52eafe7..23f4a4ba55f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt @@ -6,7 +6,6 @@ package org.thoughtcrime.securesms.help import android.widget.ImageView -import androidx.activity.compose.LocalActivity import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme @@ -37,7 +36,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -58,7 +56,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.view.WindowCompat import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.signal.core.ui.compose.Buttons @@ -82,7 +79,6 @@ fun HelpScreenContent( onWhatIsDebugLogClick: () -> Unit, onFaqClick: () -> Unit, ) { - val activity = LocalActivity.current val context = LocalContext.current val categories = stringArrayResource(R.array.HelpFragment__categories_6).toList() @@ -106,17 +102,6 @@ fun HelpScreenContent( } } - DisposableEffect(Unit) { - activity?.window?.let { - WindowCompat.setDecorFitsSystemWindows(it, false) - } - onDispose { - activity?.window?.let { - WindowCompat.setDecorFitsSystemWindows(it, true) - } - } - } - Scaffolds.Settings( snackbarHost = { Snackbars.Host(snackbarHostState = snackbarHostState) }, title = stringResource(R.string.preferences__help), @@ -125,7 +110,6 @@ fun HelpScreenContent( ) { paddingValues -> Column( modifier = Modifier - .fillMaxSize() .padding(paddingValues) ) { Column( From 2d8f89a90f1e23c5f9d48d8920f0a5f0937462af Mon Sep 17 00:00:00 2001 From: ArseniiS Date: Wed, 13 May 2026 17:32:08 +0200 Subject: [PATCH 11/11] Format code --- .../java/org/thoughtcrime/securesms/help/HelpScreen.kt | 4 ++-- .../org/thoughtcrime/securesms/help/HelpScreenEvents.kt | 2 +- .../java/org/thoughtcrime/securesms/help/HelpViewModel.kt | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt index 23f4a4ba55f..03f3d786b14 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreen.kt @@ -77,7 +77,7 @@ fun HelpScreenContent( sideEffect: Flow, onNavigationClick: () -> Unit, onWhatIsDebugLogClick: () -> Unit, - onFaqClick: () -> Unit, + onFaqClick: () -> Unit ) { val context = LocalContext.current val categories = stringArrayResource(R.array.HelpFragment__categories_6).toList() @@ -348,7 +348,7 @@ private fun HelpScreenPreview() { sideEffect = emptyFlow(), onNavigationClick = {}, onWhatIsDebugLogClick = {}, - onFaqClick = {}, + onFaqClick = {} ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt index 35388989494..fa0e4f49f1f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpScreenEvents.kt @@ -11,4 +11,4 @@ sealed interface HelpScreenEvents { data class FeelingSelected(val feeling: Feeling) : HelpScreenEvents data class DebugLogsToggled(val toggle: Boolean) : HelpScreenEvents object OnNextClick : HelpScreenEvents -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt index c784f085ffb..08d05343d0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.kt @@ -25,9 +25,11 @@ class HelpViewModel( application: Application ) : AndroidViewModel(application) { - private val internalState = MutableStateFlow(HelpScreenState( - categoryIndex = startCategoryIndex - )) + private val internalState = MutableStateFlow( + HelpScreenState( + categoryIndex = startCategoryIndex + ) + ) val state = internalState.asStateFlow() private val internalSideEffect = Channel(Channel.BUFFERED)