Skip to content

Migrate Help Fragment to Compose#14756

Open
rsrsaa wants to merge 11 commits into
signalapp:mainfrom
rsrsaa:migrate-help-screen-fragment-to-compose
Open

Migrate Help Fragment to Compose#14756
rsrsaa wants to merge 11 commits into
signalapp:mainfrom
rsrsaa:migrate-help-screen-fragment-to-compose

Conversation

@rsrsaa
Copy link
Copy Markdown

@rsrsaa rsrsaa commented May 5, 2026

First time contributor checklist

Contributor checklist

  • Google Pixel 7, Android 16
  • Google Pixel 4a, Android 13
  • My contribution is fully baked and ready to be merged as is

Description

Migrated HelpFragment screen to Compose & Kotlin.

Tested on playStagingDebug build variant.
Also checked compose screen in layout inspector and for overdraw.

Screenshots before/after attached.

Before After
Before After

@greyson-signal
Copy link
Copy Markdown
Contributor

Hi there, thank you for your contribution, but I don't really want to take in things like full screen conversions.

@rsrsaa
Copy link
Copy Markdown
Author

rsrsaa commented May 5, 2026

@greyson-signal Hi, but this screen is pretty small one, I can add more details on what was done, on testing, etc.
I also asked on forum whether this PR is good contribution and @alex-signal said go ahead.

@rsrsaa
Copy link
Copy Markdown
Author

rsrsaa commented May 5, 2026

@greyson-signal Also, I’m available ASAP if any bugs occur

@alex-signal alex-signal reopened this May 7, 2026
Copy link
Copy Markdown
Contributor

@alex-signal alex-signal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right direction but we've adopted a Component(state, (event) -> Unit) pattern that we want to formalize throughout. Also concerned with some of the window adjustments, since this fragment will be shown in a split-pane in the near future.

override fun onContactSupport() {
requireActivity().finish()
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.REMOTE_BACKUPS_INDEX))
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), REMOTE_BACKUPS_INDEX))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be reverted, we don't directly import statics (or at least try not to)


@Composable
override fun FragmentContent() {
val startCategoryIndex = arguments?.getInt(START_CATEGORY_INDEX, 0) ?: 6
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 magic number. Should this be PAYMENT_INDEX?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, thank you, fixed

onFaqClick = {
val intent = Intent(Intent.ACTION_VIEW)
intent.setData(Uri.parse(getString(R.string.HelpFragment__link__faq)))
startActivity(intent)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommunicationActions.openBrowserLink

onNavigationClick = { requireActivity().onBackPressedDispatcher.onBackPressed() },
onWhatIsDebugLogClick = {
val intent = Intent(Intent.ACTION_VIEW)
intent.setData(Uri.parse(getString(R.string.HelpFragment__link__debug_info)))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommunicationActions.openBrowserLink


class HelpViewModel(application: Application) : AndroidViewModel(application) {

private val _state = MutableStateFlow(HelpScreenState())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internalState


DisposableEffect(Unit) {
activity?.window?.let {
WindowCompat.setDecorFitsSystemWindows(it, false)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't feel right at all. You should be using the insets modifiers if you need to space things correctly. Scaffolds.Settings should already be doing that for you.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree with you, we don't need this, removed.

.fillMaxSize()
.padding(paddingValues),
) {
Column(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LazyColumn is likely a better and more performant choice.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it is OK to leave Column here?
Pros:

  • content is fixed and small
  • it has .verticalScroll()

Cons:

  • don't see for now :)

val snackbarHostState = remember { SnackbarHostState() }

LaunchedEffect(startCategoryIndex) {
viewModel.onCategorySelected(startCategoryIndex)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just be passed to the ViewModel at construction time?


package org.thoughtcrime.securesms.help

sealed interface HelpScreenEvents {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*Events is reserved for user-initiated actions like clicks and toggles.

}

val subject = context.getString(R.string.HelpFragment__signal_android_support_request)
val body = SupportEmailUtil.generateSupportEmailBody(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll want to run the formatter with ./gradlew format

@rsrsaa
Copy link
Copy Markdown
Author

rsrsaa commented May 13, 2026

@alex-signal thank you for your time and comments!

I’ve resolved all comments and I have a few to discuss.
I’m open to discuss it and if needed make new changes/refactoring to comply with your architecture and project vision :)

We can’t have HelpScreenRepository with sendEmail() fun to call from VM because we need activity context to open email app.

So I made SideEffects flow which VM can use to send event and Compose screen can address it.

Also I left ShowSnackbar in the same SideEffects flow because

  • it more belongs to UI/Compose than VM
  • anyway we have this sideEffect flow
  • it is more natural approach with snackbar handling in Compose I think.

Events was refactored to user interaction events

So what do you think on having SideEffect flow in VM to communicate from VM to Compose screen?

Can we leave Column as root compose component in Scaffold.Settings because screen is pretty small and content is fixed?

I’ve added onEvent() param in Compose function, but left navigation back and open browsers clicks.

@Composable
fun HelpScreenContent(
  state: HelpScreenState,
  onEvent: (HelpScreenEvents) -> Unit,
  sideEffect: Flow<HelpScreenSideEffects>,
  onNavigationClick: () -> Unit,
  onWhatIsDebugLogClick: () -> Unit,
  onFaqClick: () -> Unit,
) {

@rsrsaa rsrsaa requested a review from alex-signal May 13, 2026 01:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants