Convert ChatsSettings screen to compose.

This commit is contained in:
Alex Hart
2025-08-22 12:59:03 -03:00
committed by Michelle Tang
parent 8723fd9a24
commit 2872020c1f
9 changed files with 277 additions and 117 deletions

View File

@@ -30,7 +30,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.colorResource
@@ -72,7 +71,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImag
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.InAppPaymentsBottomSheetDelegate
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
@@ -107,16 +106,11 @@ class AppSettingsFragment : ComposeFragment(), Callbacks {
)
}
val nestedScrollConnection = remember {
StatusBarColorNestedScrollConnection(requireActivity())
}
AppSettingsContent(
self = self!!,
state = state!!,
bannerManager = bannerManager,
callbacks = this,
lazyColumnModifier = Modifier.nestedScroll(nestedScrollConnection)
callbacks = this
)
}
@@ -176,8 +170,7 @@ private fun AppSettingsContent(
self: BioRecipientState,
state: AppSettingsState,
bannerManager: BannerManager,
callbacks: Callbacks,
lazyColumnModifier: Modifier = Modifier
callbacks: Callbacks
) {
val isRegisteredAndUpToDate by rememberUpdatedState(state.isRegisteredAndUpToDate())
@@ -193,7 +186,7 @@ private fun AppSettingsContent(
bannerManager.Banner()
LazyColumn(
modifier = lazyColumnModifier
modifier = rememberStatusBarColorNestedScrollModifier()
) {
item {
BioRow(

View File

@@ -47,6 +47,7 @@ import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.Texts
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -278,7 +279,10 @@ fun AccountSettingsScreen(
navigationIcon = ImageVector.vectorResource(R.drawable.ic_arrow_left_24)
) { contentPadding ->
LazyColumn(
modifier = Modifier.padding(contentPadding).testTag(AccountSettingsTestTags.SCROLLER)
modifier = Modifier
.padding(contentPadding)
.then(rememberStatusBarColorNestedScrollModifier())
.testTag(AccountSettingsTestTags.SCROLLER)
) {
item {
Texts.SectionHeader(

View File

@@ -24,6 +24,7 @@ import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.appearance.navbar.ChooseNavigationBarStyleFragment
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.keyvalue.SettingsValues
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -107,7 +108,9 @@ private fun AppearanceSettingsScreen(
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24)
) { paddingValues ->
LazyColumn(
modifier = Modifier.padding(paddingValues)
modifier = Modifier
.padding(paddingValues)
.then(rememberStatusBarColorNestedScrollModifier())
) {
item {
Rows.RadioListRow(

View File

@@ -1,125 +1,247 @@
package org.thoughtcrime.securesms.components.settings.app.chats
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.compose.Dividers
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.Texts
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__chats) {
/**
* Displays a list of chats settings options to the user, including
* generating link previews and keeping muted chats archived.
*/
class ChatsSettingsFragment : ComposeFragment() {
private lateinit var viewModel: ChatsSettingsViewModel
private val viewModel: ChatsSettingsViewModel by viewModels()
override fun onResume() {
super.onResume()
viewModel.refresh()
}
@Suppress("ReplaceGetOrSet")
override fun bindAdapter(adapter: MappingAdapter) {
viewModel = ViewModelProvider(this).get(ChatsSettingsViewModel::class.java)
@Composable
override fun FragmentContent() {
val state by viewModel.state.collectAsStateWithLifecycle()
val callbacks = remember { Callbacks() }
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
}
ChatsSettingsScreen(
state = state,
callbacks = callbacks,
isRemoteBackupsAvailable = RemoteConfig.messageBackups
)
}
private fun getConfiguration(state: ChatsSettingsState): DSLConfiguration {
return configure {
switchPref(
title = DSLSettingsText.from(R.string.preferences__generate_link_previews),
summary = DSLSettingsText.from(R.string.preferences__retrieve_link_previews_from_websites_for_messages),
isEnabled = state.isRegisteredAndUpToDate(),
isChecked = state.generateLinkPreviews,
onClick = {
viewModel.setGenerateLinkPreviewsEnabled(!state.generateLinkPreviews)
}
)
private inner class Callbacks : ChatsSettingsCallbacks {
override fun onNavigationClick() {
requireActivity().onBackPressedDispatcher.onBackPressed()
}
switchPref(
title = DSLSettingsText.from(R.string.preferences__pref_use_address_book_photos),
summary = DSLSettingsText.from(R.string.preferences__display_contact_photos_from_your_address_book_if_available),
isEnabled = state.isRegisteredAndUpToDate(),
isChecked = state.useAddressBook,
onClick = {
viewModel.setUseAddressBook(!state.useAddressBook)
}
)
override fun onGenerateLinkPreviewsChanged(enabled: Boolean) {
viewModel.setGenerateLinkPreviewsEnabled(enabled)
}
switchPref(
title = DSLSettingsText.from(R.string.preferences__pref_keep_muted_chats_archived),
summary = DSLSettingsText.from(R.string.preferences__muted_chats_that_are_archived_will_remain_archived),
isEnabled = state.isRegisteredAndUpToDate(),
isChecked = state.keepMutedChatsArchived,
onClick = {
viewModel.setKeepMutedChatsArchived(!state.keepMutedChatsArchived)
}
)
override fun onUseAddressBookChanged(enabled: Boolean) {
viewModel.setUseAddressBook(enabled)
}
dividerPref()
override fun onKeepMutedChatsArchivedChanged(enabled: Boolean) {
viewModel.setKeepMutedChatsArchived(enabled)
}
sectionHeaderPref(R.string.ChatsSettingsFragment__chat_folders)
override fun onAddAChatFolderClick() {
findNavController().safeNavigate(R.id.action_chatsSettingsFragment_to_chatFoldersFragment)
}
if (state.folderCount == 1) {
clickPref(
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__add_chat_folder),
isEnabled = state.isRegisteredAndUpToDate(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_chatFoldersFragment)
}
)
} else {
clickPref(
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__add_edit_chat_folder),
summary = DSLSettingsText.from(resources.getQuantityString(R.plurals.ChatsSettingsFragment__d_folder, state.folderCount, state.folderCount)),
isEnabled = state.isRegisteredAndUpToDate(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_chatFoldersFragment)
}
override fun onAddOrEditFoldersClick() {
findNavController().safeNavigate(R.id.action_chatsSettingsFragment_to_chatFoldersFragment)
}
override fun onUseSystemEmojiChanged(enabled: Boolean) {
viewModel.setUseSystemEmoji(enabled)
}
override fun onEnterKeySendsChanged(enabled: Boolean) {
viewModel.setEnterKeySends(enabled)
}
override fun onChatBackupsClick() {
findNavController().safeNavigate(R.id.action_chatsSettingsFragment_to_backupsPreferenceFragment)
}
}
}
private interface ChatsSettingsCallbacks {
fun onNavigationClick() = Unit
fun onGenerateLinkPreviewsChanged(enabled: Boolean) = Unit
fun onUseAddressBookChanged(enabled: Boolean) = Unit
fun onKeepMutedChatsArchivedChanged(enabled: Boolean) = Unit
fun onAddAChatFolderClick() = Unit
fun onAddOrEditFoldersClick() = Unit
fun onUseSystemEmojiChanged(enabled: Boolean) = Unit
fun onEnterKeySendsChanged(enabled: Boolean) = Unit
fun onChatBackupsClick() = Unit
object Empty : ChatsSettingsCallbacks
}
@Composable
private fun ChatsSettingsScreen(
isRemoteBackupsAvailable: Boolean,
state: ChatsSettingsState,
callbacks: ChatsSettingsCallbacks
) {
Scaffolds.Settings(
title = stringResource(R.string.preferences_chats__chats),
onNavigationClick = callbacks::onNavigationClick,
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24)
) { paddingValues ->
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.then(rememberStatusBarColorNestedScrollModifier())
) {
item {
Rows.ToggleRow(
text = stringResource(R.string.preferences__generate_link_previews),
label = stringResource(R.string.preferences__retrieve_link_previews_from_websites_for_messages),
enabled = state.isRegisteredAndUpToDate(),
checked = state.generateLinkPreviews,
onCheckChanged = callbacks::onGenerateLinkPreviewsChanged
)
}
dividerPref()
sectionHeaderPref(R.string.ChatsSettingsFragment__keyboard)
switchPref(
title = DSLSettingsText.from(R.string.preferences_advanced__use_system_emoji),
isEnabled = state.isRegisteredAndUpToDate(),
isChecked = state.useSystemEmoji,
onClick = {
viewModel.setUseSystemEmoji(!state.useSystemEmoji)
}
)
switchPref(
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__send_with_enter),
isEnabled = state.isRegisteredAndUpToDate(),
isChecked = state.enterKeySends,
onClick = {
viewModel.setEnterKeySends(!state.enterKeySends)
}
)
if (!RemoteConfig.messageBackups) {
dividerPref()
sectionHeaderPref(R.string.preferences_chats__backups)
clickPref(
title = DSLSettingsText.from(R.string.preferences_chats__chat_backups),
summary = DSLSettingsText.from(if (state.localBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled),
isEnabled = state.localBackupsEnabled || state.isRegisteredAndUpToDate(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_backupsPreferenceFragment)
}
item {
Rows.ToggleRow(
text = stringResource(R.string.preferences__pref_use_address_book_photos),
label = stringResource(R.string.preferences__display_contact_photos_from_your_address_book_if_available),
enabled = state.isRegisteredAndUpToDate(),
checked = state.useAddressBook,
onCheckChanged = callbacks::onUseAddressBookChanged
)
}
item {
Rows.ToggleRow(
text = stringResource(R.string.preferences__pref_keep_muted_chats_archived),
label = stringResource(R.string.preferences__muted_chats_that_are_archived_will_remain_archived),
enabled = state.isRegisteredAndUpToDate(),
checked = state.keepMutedChatsArchived,
onCheckChanged = callbacks::onKeepMutedChatsArchivedChanged
)
}
item {
Dividers.Default()
}
item {
Texts.SectionHeader(stringResource(R.string.ChatsSettingsFragment__chat_folders))
}
if (state.folderCount == 1) {
item {
Rows.TextRow(
text = stringResource(R.string.ChatsSettingsFragment__add_chat_folder),
enabled = state.isRegisteredAndUpToDate(),
onClick = callbacks::onAddAChatFolderClick
)
}
} else {
item {
Rows.TextRow(
text = stringResource(R.string.ChatsSettingsFragment__add_edit_chat_folder),
label = pluralStringResource(R.plurals.ChatsSettingsFragment__d_folder, state.folderCount, state.folderCount),
enabled = state.isRegisteredAndUpToDate(),
onClick = callbacks::onAddOrEditFoldersClick
)
}
}
item {
Dividers.Default()
}
item {
Texts.SectionHeader(stringResource(R.string.ChatsSettingsFragment__keyboard))
}
item {
Rows.ToggleRow(
text = stringResource(R.string.preferences_advanced__use_system_emoji),
enabled = state.isRegisteredAndUpToDate(),
checked = state.useSystemEmoji,
onCheckChanged = callbacks::onUseSystemEmojiChanged
)
}
item {
Rows.ToggleRow(
text = stringResource(R.string.ChatsSettingsFragment__send_with_enter),
enabled = state.isRegisteredAndUpToDate(),
checked = state.enterKeySends,
onCheckChanged = callbacks::onEnterKeySendsChanged
)
}
if (!isRemoteBackupsAvailable) {
item {
Dividers.Default()
}
item {
Texts.SectionHeader(stringResource(R.string.preferences_chats__backups))
}
item {
Rows.TextRow(
text = stringResource(R.string.preferences_chats__chat_backups),
label = stringResource(if (state.localBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled),
enabled = state.localBackupsEnabled || state.isRegisteredAndUpToDate(),
onClick = callbacks::onChatBackupsClick
)
}
}
}
}
}
@SignalPreview
@Composable
private fun ChatsSettingsScreenPreview() {
Previews.Preview {
ChatsSettingsScreen(
state = ChatsSettingsState(
generateLinkPreviews = true,
useAddressBook = true,
keepMutedChatsArchived = true,
useSystemEmoji = false,
enterKeySends = false,
localBackupsEnabled = true,
folderCount = 1,
userUnregistered = false,
clientDeprecated = false
),
callbacks = ChatsSettingsCallbacks.Empty,
isRemoteBackupsAvailable = false
)
}
}

View File

@@ -1,9 +1,11 @@
package org.thoughtcrime.securesms.components.settings.app.chats
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFoldersRepository
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -12,7 +14,6 @@ import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.ThrottledDebouncer
import org.thoughtcrime.securesms.util.livedata.Store
class ChatsSettingsViewModel @JvmOverloads constructor(
private val repository: ChatsSettingsRepository = ChatsSettingsRepository()
@@ -20,7 +21,7 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
private val refreshDebouncer = ThrottledDebouncer(500L)
private val store: Store<ChatsSettingsState> = Store(
private val store = MutableStateFlow(
ChatsSettingsState(
generateLinkPreviews = SignalStore.settings.isLinkPreviewsEnabled,
useAddressBook = SignalStore.settings.isPreferSystemContactPhotos,
@@ -34,7 +35,7 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
)
)
val state: LiveData<ChatsSettingsState> = store.stateLiveData
val state: StateFlow<ChatsSettingsState> = store
fun setGenerateLinkPreviewsEnabled(enabled: Boolean) {
store.update { it.copy(generateLinkPreviews = enabled) }
@@ -70,7 +71,7 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
val count = ChatFoldersRepository.getFolderCount()
val backupsEnabled = SignalStore.settings.isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(AppDependencies.application)
if (store.state.localBackupsEnabled != backupsEnabled) {
if (store.value.localBackupsEnabled != backupsEnabled) {
store.update {
it.copy(
folderCount = count,

View File

@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.emoji.EmojiStrings
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.AddAllowedMembersViewModel.NotificationProfileAndRecipients
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
@@ -166,7 +167,9 @@ private fun AddAllowedMembersContent(
modifier = Modifier.padding(contentPadding)
) {
LazyColumn(
modifier = Modifier.weight(1f)
modifier = Modifier
.weight(1f)
.then(rememberStatusBarColorNestedScrollModifier())
) {
item {
Text(

View File

@@ -42,6 +42,7 @@ import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.Texts
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.viewModel
@@ -176,7 +177,9 @@ private fun AdvancedPrivacySettingsScreen(
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24)
) { paddingValues ->
LazyColumn(
modifier = Modifier.padding(paddingValues)
modifier = Modifier
.padding(paddingValues)
.then(rememberStatusBarColorNestedScrollModifier())
) {
item {
Rows.ToggleRow(

View File

@@ -23,6 +23,7 @@ import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.ApkUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -90,7 +91,9 @@ private fun AppUpdatesSettingsScreen(
) { paddingValues ->
LazyColumn(
modifier = Modifier.padding(paddingValues)
modifier = Modifier
.padding(paddingValues)
.then(rememberStatusBarColorNestedScrollModifier())
) {
if (Build.VERSION.SDK_INT >= 31) {
item {

View File

@@ -2,9 +2,15 @@ package org.thoughtcrime.securesms.compose
import android.animation.ValueAnimator
import android.app.Activity
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Velocity
import androidx.core.content.ContextCompat
import com.google.android.material.animation.ArgbEvaluatorCompat
@@ -14,10 +20,14 @@ import kotlin.math.abs
/**
* Controls status-bar color based off a nested scroll
*
* Recommended to use this with [rememberStatusBarColorNestedScrollModifier] since it'll prevent you from having to wire through
* an activity or the connection to subcomponents.
*/
class StatusBarColorNestedScrollConnection(
private val activity: Activity
) : NestedScrollConnection {
private var animator: ValueAnimator? = null
private val normalColor = ContextCompat.getColor(activity, R.color.signal_colorBackground)
@@ -78,3 +88,21 @@ class StatusBarColorNestedScrollConnection(
private fun Float.isNearZero(): Boolean = abs(this) < 0.001
}
/**
* Remembers the nested scroll modifier to ensure the proper status bar coloring behavior.
*
* This is only required if the screen you are modifying does not utilize edgeToEdge.
*/
@Composable
fun rememberStatusBarColorNestedScrollModifier(): Modifier {
val activity = LocalContext.current as? AppCompatActivity
return remember {
if (activity != null) {
Modifier.nestedScroll(StatusBarColorNestedScrollConnection(activity))
} else {
Modifier
}
}
}