Rewrite App Settings in compose.
This commit is contained in:
parent
7f3ceea9fe
commit
b979be0cb9
13 changed files with 710 additions and 486 deletions
|
@ -568,6 +568,7 @@ dependencies {
|
||||||
implementation(libs.dnsjava)
|
implementation(libs.dnsjava)
|
||||||
implementation(libs.kotlinx.collections.immutable)
|
implementation(libs.kotlinx.collections.immutable)
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
|
implementation(libs.accompanist.drawablepainter)
|
||||||
implementation(libs.kotlin.stdlib.jdk8)
|
implementation(libs.kotlin.stdlib.jdk8)
|
||||||
implementation(libs.kotlin.reflect)
|
implementation(libs.kotlin.reflect)
|
||||||
implementation(libs.kotlinx.coroutines.play.services)
|
implementation(libs.kotlinx.coroutines.play.services)
|
||||||
|
|
|
@ -7,8 +7,10 @@ package org.thoughtcrime.securesms.banner
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
@ -60,4 +62,19 @@ class BannerManager @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the current banner.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Banner() {
|
||||||
|
val banner by rememberUpdatedState(banners.firstOrNull { it.enabled } as Banner<Any>?)
|
||||||
|
|
||||||
|
banner?.let { nonNullBanner ->
|
||||||
|
val state by nonNullBanner.dataFlow.collectAsStateWithLifecycle(initialValue = null)
|
||||||
|
state?.let { model ->
|
||||||
|
nonNullBanner.DisplayBanner(model, PaddingValues(horizontal = 12.dp, vertical = 8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.components.emoji
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.Placeholder
|
||||||
|
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
|
import org.signal.core.ui.Previews
|
||||||
|
import org.signal.core.ui.SignalPreview
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies Signal or System emoji to the given content based off user settings.
|
||||||
|
*
|
||||||
|
* Text is transformed and passed to content as an annotated string and inline content map.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Emojifier(
|
||||||
|
text: String,
|
||||||
|
content: @Composable (AnnotatedString, Map<String, InlineTextContent>) -> Unit = { annotatedText, inlineContent ->
|
||||||
|
Text(
|
||||||
|
text = annotatedText,
|
||||||
|
inlineContent = inlineContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (LocalInspectionMode.current) {
|
||||||
|
content(buildAnnotatedString { append(text) }, emptyMap())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val candidates = remember(text) { EmojiProvider.getCandidates(text) }
|
||||||
|
val candidateMap: Map<String, InlineTextContent> = remember(text) {
|
||||||
|
candidates?.associate { candidate ->
|
||||||
|
candidate.drawInfo.emoji to InlineTextContent(placeholder = Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)) {
|
||||||
|
Image(
|
||||||
|
painter = rememberDrawablePainter(EmojiProvider.getEmojiDrawable(context, candidate.drawInfo.emoji)),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
val annotatedString = buildAnnotatedString {
|
||||||
|
append(text)
|
||||||
|
|
||||||
|
candidates?.forEach {
|
||||||
|
addStringAnnotation(
|
||||||
|
tag = "EMOJI",
|
||||||
|
annotation = it.drawInfo.emoji,
|
||||||
|
start = it.startIndex,
|
||||||
|
end = it.endIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content(annotatedString, candidateMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@SignalPreview
|
||||||
|
private fun EmojifierPreview() {
|
||||||
|
Previews.Preview {
|
||||||
|
Emojifier(text = "This message has an emoji ❤\uFE0F")
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,292 +2,139 @@ package org.thoughtcrime.securesms.components.settings.app
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.IdRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.NavDirections
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.signal.core.util.isNotNullOrBlank
|
import org.signal.core.ui.Dividers
|
||||||
|
import org.signal.core.ui.IconButtons
|
||||||
|
import org.signal.core.ui.Previews
|
||||||
|
import org.signal.core.ui.Rows
|
||||||
|
import org.signal.core.ui.Scaffolds
|
||||||
|
import org.signal.core.ui.SignalPreview
|
||||||
|
import org.signal.core.ui.horizontalGutters
|
||||||
|
import org.signal.core.ui.theme.SignalTheme
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
import org.thoughtcrime.securesms.avatar.AvatarImage
|
||||||
|
import org.thoughtcrime.securesms.banner.Banner
|
||||||
import org.thoughtcrime.securesms.banner.BannerManager
|
import org.thoughtcrime.securesms.banner.BannerManager
|
||||||
import org.thoughtcrime.securesms.banner.banners.DeprecatedBuildBanner
|
import org.thoughtcrime.securesms.banner.banners.DeprecatedBuildBanner
|
||||||
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner
|
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner
|
||||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
import org.thoughtcrime.securesms.banner.ui.compose.Importance
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
import org.thoughtcrime.securesms.components.emoji.Emojifier
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageMedium
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
|
||||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
|
||||||
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.InAppPaymentsBottomSheetDelegate
|
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.InAppPaymentsBottomSheetDelegate
|
||||||
import org.thoughtcrime.securesms.components.settings.configure
|
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|
||||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||||
|
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.Environment
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
|
||||||
import org.thoughtcrime.securesms.util.Util
|
import org.thoughtcrime.securesms.util.Util
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil
|
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
|
||||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||||
import org.thoughtcrime.securesms.util.views.Stub
|
|
||||||
import org.thoughtcrime.securesms.util.visible
|
|
||||||
|
|
||||||
class AppSettingsFragment : DSLSettingsFragment(
|
class AppSettingsFragment : ComposeFragment(), Callbacks {
|
||||||
titleId = R.string.text_secure_normal__menu_settings,
|
|
||||||
layoutId = R.layout.dsl_settings_fragment_with_reminder
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val viewModel: AppSettingsViewModel by viewModels()
|
private val viewModel: AppSettingsViewModel by viewModels()
|
||||||
|
|
||||||
private var bannerManager: BannerManager? = null
|
|
||||||
private lateinit var bannerView: Stub<ComposeView>
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
viewLifecycleOwner.lifecycle.addObserver(InAppPaymentsBottomSheetDelegate(childFragmentManager, viewLifecycleOwner))
|
viewLifecycleOwner.lifecycle.addObserver(InAppPaymentsBottomSheetDelegate(childFragmentManager, viewLifecycleOwner))
|
||||||
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
bannerView = ViewUtil.findStubById(view, R.id.banner_stub)
|
|
||||||
|
|
||||||
initializeBanners()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bindAdapter(adapter: MappingAdapter) {
|
@Composable
|
||||||
adapter.registerFactory(BioPreference::class.java, LayoutFactory(::BioPreferenceViewHolder, R.layout.bio_preference_item))
|
override fun FragmentContent() {
|
||||||
adapter.registerFactory(PaymentsPreference::class.java, LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference))
|
val state by viewModel.state.observeAsState()
|
||||||
adapter.registerFactory(SubscriptionPreference::class.java, LayoutFactory(::SubscriptionPreferenceViewHolder, R.layout.dsl_preference_item))
|
val self by viewModel.self.observeAsState()
|
||||||
|
|
||||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
if (state == null) return
|
||||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initializeBanners() {
|
val context = LocalContext.current
|
||||||
this.bannerManager = BannerManager(
|
val bannerManager = remember {
|
||||||
|
BannerManager(
|
||||||
banners = listOf(
|
banners = listOf(
|
||||||
DeprecatedBuildBanner(),
|
DeprecatedBuildBanner(),
|
||||||
UnauthorizedBanner(requireContext())
|
UnauthorizedBanner(context)
|
||||||
),
|
|
||||||
onNewBannerShownListener = {
|
|
||||||
if (bannerView.resolved()) {
|
|
||||||
bannerView.get().addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->
|
|
||||||
recyclerView?.setPadding(0, bottom - top, 0, 0)
|
|
||||||
}
|
|
||||||
recyclerView?.clipToPadding = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onNoBannerShownListener = {
|
|
||||||
recyclerView?.clipToPadding = true
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
this.bannerManager?.updateContent(bannerView.get())
|
AppSettingsContent(
|
||||||
|
self = self!!,
|
||||||
|
state = state!!,
|
||||||
|
bannerManager = bannerManager,
|
||||||
|
callbacks = this
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.refreshDeprecatedOrUnregistered()
|
override fun onNavigationClick() {
|
||||||
|
requireActivity().finishAfterTransition()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun navigate(actionId: Int) {
|
||||||
|
findNavController().safeNavigate(actionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun navigate(directions: NavDirections) {
|
||||||
|
findNavController().safeNavigate(directions)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
viewModel.refreshExpiredGiftBadge()
|
viewModel.refreshExpiredGiftBadge()
|
||||||
this.bannerManager?.updateContent(bannerView.get())
|
viewModel.refreshDeprecatedOrUnregistered()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getConfiguration(state: AppSettingsState): DSLConfiguration {
|
override fun copyDonorBadgeSubscriberIdToClipboard() {
|
||||||
return configure {
|
|
||||||
customPref(
|
|
||||||
BioPreference(
|
|
||||||
recipient = state.self,
|
|
||||||
onRowClicked = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
|
|
||||||
},
|
|
||||||
onQrButtonClicked = {
|
|
||||||
if (SignalStore.account.username != null) {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameLinkSettingsFragment)
|
|
||||||
} else {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameEducationFragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__account),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_person_circle_24),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_accountSettingsFragment)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences__linked_devices),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_devices_24),
|
|
||||||
onClick = { findNavController().safeNavigate(R.id.action_appSettingsFragment_to_linkDeviceFragment) },
|
|
||||||
isEnabled = state.isRegisteredAndUpToDate()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (state.allowUserToGoToDonationManagementScreen) {
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences__donate_to_signal),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_heart_24),
|
|
||||||
iconEnd = if (state.hasExpiredGiftBadge) DSLSettingsIcon.from(R.drawable.symbol_info_fill_24, R.color.signal_accent_primary) else null,
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToManageDonationsFragment())
|
|
||||||
},
|
|
||||||
onLongClick = this@AppSettingsFragment::copyDonorBadgeSubscriberIdToClipboard
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
externalLinkPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences__donate_to_signal),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_heart_24),
|
|
||||||
linkId = R.string.donate_url
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
dividerPref()
|
|
||||||
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences__appearance),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_appearance_24),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences_chats__chats),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_chat_24),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
|
|
||||||
},
|
|
||||||
onLongClick = this@AppSettingsFragment::copyRemoteBackupsSubscriberIdToClipboard,
|
|
||||||
isEnabled = state.isRegisteredAndUpToDate()
|
|
||||||
)
|
|
||||||
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences__stories),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_stories_24),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToStoryPrivacySettings(R.string.preferences__stories))
|
|
||||||
},
|
|
||||||
isEnabled = state.isRegisteredAndUpToDate()
|
|
||||||
)
|
|
||||||
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences__notifications),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_bell_24),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment)
|
|
||||||
},
|
|
||||||
isEnabled = state.isRegisteredAndUpToDate()
|
|
||||||
)
|
|
||||||
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences__privacy),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_lock_white_48),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_privacySettingsFragment)
|
|
||||||
},
|
|
||||||
isEnabled = state.isRegisteredAndUpToDate()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (RemoteConfig.messageBackups) {
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences_chats__backups),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_backup_24),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_backupsSettingsFragment)
|
|
||||||
},
|
|
||||||
isEnabled = state.isRegisteredAndUpToDate()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences__data_and_storage),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_data_24),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Environment.IS_NIGHTLY) {
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from("App updates"),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_calendar_24),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_appUpdatesSettingsFragment)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
dividerPref()
|
|
||||||
|
|
||||||
if (SignalStore.payments.paymentsAvailability.showPaymentsMenu()) {
|
|
||||||
customPref(
|
|
||||||
PaymentsPreference(
|
|
||||||
unreadCount = state.unreadPaymentsCount
|
|
||||||
) {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_paymentsActivity)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
dividerPref()
|
|
||||||
}
|
|
||||||
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences__help),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_help_24),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_helpSettingsFragment)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.AppSettingsFragment__invite_your_friends),
|
|
||||||
icon = DSLSettingsIcon.from(R.drawable.symbol_invite_24),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_inviteActivity)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (RemoteConfig.internalUser) {
|
|
||||||
dividerPref()
|
|
||||||
|
|
||||||
clickPref(
|
|
||||||
title = DSLSettingsText.from(R.string.preferences__internal_preferences),
|
|
||||||
onClick = {
|
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_internalSettingsFragment)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyDonorBadgeSubscriberIdToClipboard(): Boolean {
|
|
||||||
copySubscriberIdToClipboard(
|
copySubscriberIdToClipboard(
|
||||||
subscriberType = InAppPaymentSubscriberRecord.Type.DONATION,
|
subscriberType = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||||
toastSuccessStringRes = R.string.AppSettingsFragment__copied_donor_subscriber_id_to_clipboard
|
toastSuccessStringRes = R.string.AppSettingsFragment__copied_donor_subscriber_id_to_clipboard
|
||||||
)
|
)
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyRemoteBackupsSubscriberIdToClipboard(): Boolean {
|
override fun copyRemoteBackupsSubscriberIdToClipboard() {
|
||||||
copySubscriberIdToClipboard(
|
copySubscriberIdToClipboard(
|
||||||
subscriberType = InAppPaymentSubscriberRecord.Type.BACKUP,
|
subscriberType = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||||
toastSuccessStringRes = R.string.AppSettingsFragment__copied_backups_subscriber_id_to_clipboard
|
toastSuccessStringRes = R.string.AppSettingsFragment__copied_backups_subscriber_id_to_clipboard
|
||||||
)
|
)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copySubscriberIdToClipboard(
|
private fun copySubscriberIdToClipboard(
|
||||||
|
@ -307,109 +154,463 @@ class AppSettingsFragment : DSLSettingsFragment(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SubscriptionPreference(
|
|
||||||
override val title: DSLSettingsText,
|
|
||||||
override val summary: DSLSettingsText? = null,
|
|
||||||
override val icon: DSLSettingsIcon? = null,
|
|
||||||
override val isEnabled: Boolean = true,
|
|
||||||
val isActive: Boolean = false,
|
|
||||||
val onClick: (Boolean) -> Unit,
|
|
||||||
val onLongClick: () -> Boolean
|
|
||||||
) : PreferenceModel<SubscriptionPreference>() {
|
|
||||||
override fun areItemsTheSame(newItem: SubscriptionPreference): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(newItem: SubscriptionPreference): Boolean {
|
@Composable
|
||||||
return super.areContentsTheSame(newItem) && isActive == newItem.isActive
|
private fun AppSettingsContent(
|
||||||
}
|
self: BioRecipientState,
|
||||||
|
state: AppSettingsState,
|
||||||
|
bannerManager: BannerManager,
|
||||||
|
callbacks: Callbacks
|
||||||
|
) {
|
||||||
|
val isRegisteredAndUpToDate by rememberUpdatedState(state.isRegisteredAndUpToDate())
|
||||||
|
|
||||||
|
Scaffolds.Settings(
|
||||||
|
title = stringResource(R.string.text_secure_normal__menu_settings),
|
||||||
|
navigationContentDescription = stringResource(R.string.CallScreenTopBar__go_back),
|
||||||
|
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
|
||||||
|
onNavigationClick = callbacks::onNavigationClick
|
||||||
|
) { contentPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(contentPadding)
|
||||||
|
) {
|
||||||
|
bannerManager.Banner()
|
||||||
|
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
BioRow(
|
||||||
|
self = self,
|
||||||
|
callbacks = callbacks
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SubscriptionPreferenceViewHolder(itemView: View) : PreferenceViewHolder<SubscriptionPreference>(itemView) {
|
item {
|
||||||
override fun bind(model: SubscriptionPreference) {
|
Rows.TextRow(
|
||||||
super.bind(model)
|
text = stringResource(R.string.AccountSettingsFragment__account),
|
||||||
itemView.setOnClickListener { model.onClick(model.isActive) }
|
icon = painterResource(R.drawable.symbol_person_circle_24),
|
||||||
itemView.setOnLongClickListener { model.onLongClick() }
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_accountSettingsFragment)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class BioPreference(val recipient: Recipient, val onRowClicked: () -> Unit, val onQrButtonClicked: () -> Unit) : PreferenceModel<BioPreference>() {
|
item {
|
||||||
override fun areContentsTheSame(newItem: BioPreference): Boolean {
|
Rows.TextRow(
|
||||||
return super.areContentsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
|
text = stringResource(R.string.preferences__linked_devices),
|
||||||
|
icon = painterResource(R.drawable.symbol_devices_24),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_linkDeviceFragment)
|
||||||
|
},
|
||||||
|
enabled = isRegisteredAndUpToDate
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areItemsTheSame(newItem: BioPreference): Boolean {
|
item {
|
||||||
return recipient == newItem.recipient
|
val context = LocalContext.current
|
||||||
|
val donateUrl = stringResource(R.string.donate_url)
|
||||||
|
|
||||||
|
Rows.TextRow(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.preferences__donate_to_signal),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.hasExpiredGiftBadge) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.symbol_info_fill_24),
|
||||||
|
tint = colorResource(R.color.signal_accent_primary),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
icon = {
|
||||||
private class BioPreferenceViewHolder(itemView: View) : PreferenceViewHolder<BioPreference>(itemView) {
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.symbol_heart_24),
|
||||||
private val avatarView: AvatarImageView = itemView.findViewById(R.id.icon)
|
contentDescription = null,
|
||||||
private val aboutView: EmojiTextView = itemView.findViewById(R.id.about)
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
private val badgeView: BadgeImageView = itemView.findViewById(R.id.badge)
|
)
|
||||||
private val qrButton: View = itemView.findViewById(R.id.qr_button)
|
},
|
||||||
private val usernameView: TextView = itemView.findViewById(R.id.username)
|
onClick = {
|
||||||
|
if (state.allowUserToGoToDonationManagementScreen) {
|
||||||
init {
|
callbacks.navigate(R.id.action_appSettingsFragment_to_manageDonationsFragment)
|
||||||
aboutView.setOverflowText(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(model: BioPreference) {
|
|
||||||
super.bind(model)
|
|
||||||
|
|
||||||
itemView.setOnClickListener { model.onRowClicked() }
|
|
||||||
|
|
||||||
titleView.text = model.recipient.profileName.toString()
|
|
||||||
summaryView.text = PhoneNumberFormatter.prettyPrint(model.recipient.requireE164())
|
|
||||||
usernameView.text = model.recipient.username.orElse("")
|
|
||||||
usernameView.visible = model.recipient.username.isPresent
|
|
||||||
avatarView.setRecipient(Recipient.self())
|
|
||||||
badgeView.setBadgeFromRecipient(Recipient.self())
|
|
||||||
|
|
||||||
titleView.visibility = View.VISIBLE
|
|
||||||
summaryView.visibility = View.VISIBLE
|
|
||||||
avatarView.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
if (SignalStore.account.username.isNotNullOrBlank()) {
|
|
||||||
qrButton.visibility = View.VISIBLE
|
|
||||||
qrButton.isClickable = true
|
|
||||||
qrButton.setOnClickListener { model.onQrButtonClicked() }
|
|
||||||
} else {
|
} else {
|
||||||
qrButton.visibility = View.GONE
|
CommunicationActions.openBrowserLink(context, donateUrl)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
callbacks.copyDonorBadgeSubscriberIdToClipboard()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.recipient.combinedAboutAndEmoji != null) {
|
item {
|
||||||
aboutView.text = model.recipient.combinedAboutAndEmoji
|
Dividers.Default()
|
||||||
aboutView.visibility = View.VISIBLE
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = stringResource(R.string.preferences__appearance),
|
||||||
|
icon = painterResource(R.drawable.symbol_appearance_24),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = stringResource(R.string.preferences_chats__chats),
|
||||||
|
icon = painterResource(R.drawable.symbol_chat_24),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
|
||||||
|
},
|
||||||
|
enabled = isRegisteredAndUpToDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = stringResource(R.string.preferences__stories),
|
||||||
|
icon = painterResource(R.drawable.symbol_stories_24),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToStoryPrivacySettings(R.string.preferences__stories))
|
||||||
|
},
|
||||||
|
enabled = isRegisteredAndUpToDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = stringResource(R.string.preferences__notifications),
|
||||||
|
icon = painterResource(R.drawable.symbol_bell_24),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment)
|
||||||
|
},
|
||||||
|
enabled = isRegisteredAndUpToDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = stringResource(R.string.preferences__privacy),
|
||||||
|
icon = painterResource(R.drawable.symbol_lock_24),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_privacySettingsFragment)
|
||||||
|
},
|
||||||
|
enabled = isRegisteredAndUpToDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.showBackups) {
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = stringResource(R.string.preferences_chats__backups),
|
||||||
|
icon = painterResource(R.drawable.symbol_backup_24),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_backupsSettingsFragment)
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
callbacks.copyRemoteBackupsSubscriberIdToClipboard()
|
||||||
|
},
|
||||||
|
enabled = isRegisteredAndUpToDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = stringResource(R.string.preferences__data_and_storage),
|
||||||
|
icon = painterResource(R.drawable.symbol_data_24),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.showAppUpdates) {
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = "App updates",
|
||||||
|
icon = painterResource(R.drawable.symbol_calendar_24),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_appUpdatesSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.showPayments) {
|
||||||
|
item {
|
||||||
|
Dividers.Default()
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.preferences__payments),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.unreadPaymentsCount > 0) {
|
||||||
|
Text(
|
||||||
|
text = state.unreadPaymentsCount.toString(),
|
||||||
|
color = MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
shape = RoundedCornerShape(50)
|
||||||
|
)
|
||||||
|
.defaultMinSize(minWidth = 30.dp)
|
||||||
|
.padding(4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.symbol_payment_24),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_paymentsActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Dividers.Default()
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = stringResource(R.string.preferences__help),
|
||||||
|
icon = painterResource(R.drawable.symbol_help_24),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_helpSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = stringResource(R.string.AppSettingsFragment__invite_your_friends),
|
||||||
|
icon = painterResource(R.drawable.symbol_invite_24),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_inviteActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.showInternalPreferences) {
|
||||||
|
item {
|
||||||
|
Dividers.Default()
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Rows.TextRow(
|
||||||
|
text = stringResource(R.string.preferences__internal_preferences),
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_internalSettingsFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BioRow(
|
||||||
|
self: BioRecipientState,
|
||||||
|
callbacks: Callbacks
|
||||||
|
) {
|
||||||
|
val hasUsername by rememberUpdatedState(self.username.isNotBlank())
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.horizontalGutters()
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
AvatarImage(
|
||||||
|
recipient = self.recipient,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 24.dp)
|
||||||
|
.size(80.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (self.featuredBadge != null) {
|
||||||
|
BadgeImageMedium(
|
||||||
|
badge = self.featuredBadge,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 24.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 24.dp, end = 12.dp)
|
||||||
|
) {
|
||||||
|
Emojifier(text = self.profileName.toString()) { annotatedString, inlineTextContentMap ->
|
||||||
|
Text(
|
||||||
|
text = annotatedString,
|
||||||
|
inlineContent = inlineTextContentMap,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val prettyPhoneNumber = if (LocalInspectionMode.current) {
|
||||||
|
self.e164
|
||||||
} else {
|
} else {
|
||||||
aboutView.visibility = View.GONE
|
remember(self.e164) {
|
||||||
|
PhoneNumberFormatter.prettyPrint(self.e164)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = prettyPhoneNumber,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasUsername) {
|
||||||
|
Text(
|
||||||
|
text = self.username,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.combinedAboutAndEmoji != null) {
|
||||||
|
Emojifier(
|
||||||
|
text = self.combinedAboutAndEmoji
|
||||||
|
) { annotatedString, inlineTextContentMap ->
|
||||||
|
Text(
|
||||||
|
text = annotatedString,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
inlineContent = inlineTextContentMap,
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PaymentsPreference(val unreadCount: Int, val onClick: () -> Unit) : PreferenceModel<PaymentsPreference>() {
|
if (hasUsername) {
|
||||||
override fun areContentsTheSame(newItem: PaymentsPreference): Boolean {
|
IconButtons.IconButton(
|
||||||
return super.areContentsTheSame(newItem) && unreadCount == newItem.unreadCount
|
onClick = {
|
||||||
|
callbacks.navigate(R.id.action_appSettingsFragment_to_usernameLinkSettingsFragment)
|
||||||
|
},
|
||||||
|
size = 36.dp,
|
||||||
|
colors = IconButtons.iconButtonColors(
|
||||||
|
containerColor = SignalTheme.colors.colorSurface4
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.symbol_qrcode_24),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areItemsTheSame(newItem: PaymentsPreference): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PaymentsPreferenceViewHolder(itemView: View) : MappingViewHolder<PaymentsPreference>(itemView) {
|
@SignalPreview
|
||||||
|
@Composable
|
||||||
|
private fun AppSettingsContentPreview() {
|
||||||
|
Previews.Preview {
|
||||||
|
AppSettingsContent(
|
||||||
|
self = BioRecipientState(
|
||||||
|
Recipient(
|
||||||
|
systemContactName = "Miles Morales",
|
||||||
|
profileName = ProfileName.fromParts("Miles", "Morales ❤\uFE0F"),
|
||||||
|
isSelf = true,
|
||||||
|
e164Value = "+15555555555",
|
||||||
|
usernameValue = "miles.98",
|
||||||
|
aboutEmoji = "❤\uFE0F",
|
||||||
|
about = "About",
|
||||||
|
isResolving = false
|
||||||
|
)
|
||||||
|
),
|
||||||
|
state = AppSettingsState(
|
||||||
|
unreadPaymentsCount = 5,
|
||||||
|
hasExpiredGiftBadge = true,
|
||||||
|
allowUserToGoToDonationManagementScreen = true,
|
||||||
|
userUnregistered = false,
|
||||||
|
clientDeprecated = false,
|
||||||
|
showInternalPreferences = true,
|
||||||
|
showPayments = true,
|
||||||
|
showAppUpdates = true,
|
||||||
|
showBackups = true
|
||||||
|
),
|
||||||
|
bannerManager = BannerManager(
|
||||||
|
banners = listOf(TestBanner())
|
||||||
|
),
|
||||||
|
callbacks = EmptyCallbacks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val unreadCountView: TextView = itemView.findViewById(R.id.unread_indicator)
|
@SignalPreview
|
||||||
|
@Composable
|
||||||
|
private fun BioRowPreview() {
|
||||||
|
Previews.Preview {
|
||||||
|
BioRow(
|
||||||
|
self = BioRecipientState(
|
||||||
|
Recipient(
|
||||||
|
systemContactName = "Miles Morales",
|
||||||
|
profileName = ProfileName.fromParts("Miles", "Morales ❤\uFE0F"),
|
||||||
|
isSelf = true,
|
||||||
|
e164Value = "+15555555555",
|
||||||
|
usernameValue = "miles.98",
|
||||||
|
aboutEmoji = "❤\uFE0F",
|
||||||
|
about = "About",
|
||||||
|
isResolving = false
|
||||||
|
)
|
||||||
|
),
|
||||||
|
callbacks = EmptyCallbacks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun bind(model: PaymentsPreference) {
|
private interface Callbacks {
|
||||||
unreadCountView.text = model.unreadCount.toString()
|
fun onNavigationClick(): Unit = error("Not implemented.")
|
||||||
unreadCountView.visibility = if (model.unreadCount > 0) View.VISIBLE else View.GONE
|
fun navigate(@IdRes actionId: Int): Unit = error("Not implemented")
|
||||||
|
fun navigate(directions: NavDirections): Unit = error("Not implemented")
|
||||||
|
fun copyDonorBadgeSubscriberIdToClipboard(): Unit = error("Not implemented")
|
||||||
|
fun copyRemoteBackupsSubscriberIdToClipboard(): Unit = error("Not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
private object EmptyCallbacks : Callbacks
|
||||||
model.onClick()
|
|
||||||
}
|
private class TestBanner : Banner<Unit>() {
|
||||||
}
|
override val enabled: Boolean = true
|
||||||
|
override val dataFlow: Flow<Unit> = flowOf(Unit)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun DisplayBanner(model: Unit, contentPadding: PaddingValues) {
|
||||||
|
DefaultBanner(
|
||||||
|
title = "Test Title",
|
||||||
|
body = "This is a test body",
|
||||||
|
importance = Importance.ERROR,
|
||||||
|
actions = listOf(
|
||||||
|
Action(android.R.string.ok) {}
|
||||||
|
),
|
||||||
|
paddingValues = contentPadding
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
package org.thoughtcrime.securesms.components.settings.app
|
package org.thoughtcrime.securesms.components.settings.app
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import androidx.compose.runtime.Immutable
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.Environment
|
||||||
|
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||||
|
|
||||||
|
@Immutable
|
||||||
data class AppSettingsState(
|
data class AppSettingsState(
|
||||||
val self: Recipient,
|
|
||||||
val unreadPaymentsCount: Int,
|
val unreadPaymentsCount: Int,
|
||||||
val hasExpiredGiftBadge: Boolean,
|
val hasExpiredGiftBadge: Boolean,
|
||||||
val allowUserToGoToDonationManagementScreen: Boolean,
|
val allowUserToGoToDonationManagementScreen: Boolean,
|
||||||
val userUnregistered: Boolean,
|
val userUnregistered: Boolean,
|
||||||
val clientDeprecated: Boolean
|
val clientDeprecated: Boolean,
|
||||||
|
val showInternalPreferences: Boolean = RemoteConfig.internalUser,
|
||||||
|
val showPayments: Boolean = SignalStore.payments.paymentsAvailability.showPaymentsMenu(),
|
||||||
|
val showAppUpdates: Boolean = Environment.IS_NIGHTLY,
|
||||||
|
val showBackups: Boolean = RemoteConfig.messageBackups
|
||||||
) {
|
) {
|
||||||
fun isRegisteredAndUpToDate(): Boolean {
|
fun isRegisteredAndUpToDate(): Boolean {
|
||||||
return !userUnregistered && !clientDeprecated
|
return !userUnregistered && !clientDeprecated
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.map
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
@ -19,7 +20,6 @@ class AppSettingsViewModel : ViewModel() {
|
||||||
|
|
||||||
private val store = Store(
|
private val store = Store(
|
||||||
AppSettingsState(
|
AppSettingsState(
|
||||||
Recipient.self(),
|
|
||||||
0,
|
0,
|
||||||
SignalStore.inAppPayments.getExpiredGiftBadge() != null,
|
SignalStore.inAppPayments.getExpiredGiftBadge() != null,
|
||||||
SignalStore.inAppPayments.isLikelyASustainer() || InAppDonations.hasAtLeastOnePaymentMethodAvailable(),
|
SignalStore.inAppPayments.isLikelyASustainer() || InAppDonations.hasAtLeastOnePaymentMethodAvailable(),
|
||||||
|
@ -29,14 +29,13 @@ class AppSettingsViewModel : ViewModel() {
|
||||||
)
|
)
|
||||||
|
|
||||||
private val unreadPaymentsLiveData = UnreadPaymentsLiveData()
|
private val unreadPaymentsLiveData = UnreadPaymentsLiveData()
|
||||||
private val selfLiveData: LiveData<Recipient> = Recipient.self().live().liveData
|
|
||||||
private val disposables = CompositeDisposable()
|
private val disposables = CompositeDisposable()
|
||||||
|
|
||||||
val state: LiveData<AppSettingsState> = store.stateLiveData
|
val state: LiveData<AppSettingsState> = store.stateLiveData
|
||||||
|
val self: LiveData<BioRecipientState> = Recipient.self().live().liveData.map { BioRecipientState(it) }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
store.update(unreadPaymentsLiveData) { payments, state -> state.copy(unreadPaymentsCount = payments.map { it.unreadCount }.orElse(0)) }
|
store.update(unreadPaymentsLiveData) { payments, state -> state.copy(unreadPaymentsCount = payments.map { it.unreadCount }.orElse(0)) }
|
||||||
store.update(selfLiveData) { self, state -> state.copy(self = self) }
|
|
||||||
|
|
||||||
disposables += RecurringInAppPaymentRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION).subscribeBy(
|
disposables += RecurringInAppPaymentRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION).subscribeBy(
|
||||||
onSuccess = { activeSubscription ->
|
onSuccess = { activeSubscription ->
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.google.common.base.Objects
|
||||||
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
|
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derived state class of recipient for BioRow
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
class BioRecipientState(
|
||||||
|
val recipient: Recipient
|
||||||
|
) {
|
||||||
|
val username: String = recipient.username.orElse("")
|
||||||
|
val featuredBadge: Badge? = recipient.featuredBadge
|
||||||
|
val profileName: ProfileName = recipient.profileName
|
||||||
|
val e164: String = recipient.requireE164()
|
||||||
|
val combinedAboutAndEmoji: String? = recipient.combinedAboutAndEmoji
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is Recipient) return false
|
||||||
|
return recipient.hasSameContent(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return Objects.hashCode(
|
||||||
|
recipient,
|
||||||
|
username,
|
||||||
|
featuredBadge,
|
||||||
|
profileName,
|
||||||
|
e164,
|
||||||
|
combinedAboutAndEmoji
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,26 @@ enum class BadgeImageSize(val sizeCode: Int) {
|
||||||
BADGE_112(5)
|
BADGE_112(5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BadgeImageMedium(
|
||||||
|
badge: Badge?,
|
||||||
|
modifier: Modifier
|
||||||
|
) {
|
||||||
|
if (LocalInspectionMode.current) {
|
||||||
|
Box(modifier = modifier.background(color = Color.Black, shape = CircleShape))
|
||||||
|
} else {
|
||||||
|
AndroidView(
|
||||||
|
factory = {
|
||||||
|
BadgeImageView(it, BadgeImageSize.MEDIUM)
|
||||||
|
},
|
||||||
|
update = {
|
||||||
|
it.setBadge(badge)
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BadgeImage112(
|
fun BadgeImage112(
|
||||||
badge: Badge?,
|
badge: Badge?,
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/dsl_preference_item_background"
|
|
||||||
android:minHeight="56dp"
|
|
||||||
tools:viewBindingIgnore="true">
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
|
||||||
android:id="@+id/icon"
|
|
||||||
android:layout_width="80dp"
|
|
||||||
android:layout_height="80dp"
|
|
||||||
android:layout_marginStart="24dp"
|
|
||||||
android:layout_marginTop="24dp"
|
|
||||||
android:layout_marginBottom="24dp"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.badges.BadgeImageView
|
|
||||||
android:id="@+id/badge"
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_marginStart="56dp"
|
|
||||||
android:layout_marginTop="56dp"
|
|
||||||
android:contentDescription="@string/ImageView__badge"
|
|
||||||
app:badge_size="medium"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/icon"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/icon" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="24dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:layout_marginBottom="14dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/qr_button"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/icon"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_goneMarginEnd="24dp">
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
|
||||||
android:id="@+id/title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAlignment="viewStart"
|
|
||||||
android:textAppearance="@style/Signal.Text.TitleLarge"
|
|
||||||
tools:text="Peter Parker" />
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
|
||||||
android:id="@+id/summary"
|
|
||||||
style="@style/Signal.Text.BodyMedium"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAlignment="viewStart"
|
|
||||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
|
||||||
tools:text="+1 (999) 555-1234" />
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
|
||||||
android:id="@+id/username"
|
|
||||||
style="@style/Signal.Text.BodyMedium"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAlignment="viewStart"
|
|
||||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:text="miles.07" />
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
|
||||||
android:id="@+id/about"
|
|
||||||
style="@style/Signal.Text.BodySmall"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textAlignment="viewStart"
|
|
||||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
|
||||||
tools:text="Crusin' the web" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/qr_button"
|
|
||||||
style="@style/Widget.Signal.Button.Icon"
|
|
||||||
android:layout_width="36dp"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:layout_marginEnd="24dp"
|
|
||||||
app:backgroundTint="@color/signal_colorSurface4"
|
|
||||||
app:icon="@drawable/symbol_qrcode_24"
|
|
||||||
app:iconSize="20dp"
|
|
||||||
app:iconTint="@color/signal_colorOnSurface"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -1,57 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/dsl_preference_item_background"
|
|
||||||
android:minHeight="56dp"
|
|
||||||
tools:viewBindingIgnore="true">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@id/icon"
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:srcCompat="@drawable/symbol_payment_24"
|
|
||||||
app:tint="@color/signal_icon_tint_primary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@id/title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="24dp"
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
android:text="@string/preferences__payments"
|
|
||||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/unread_indicator"
|
|
||||||
app:layout_constraintHorizontal_bias="0"
|
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/icon"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/unread_indicator"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
|
||||||
android:background="@drawable/unread_count_background"
|
|
||||||
android:gravity="center"
|
|
||||||
android:minWidth="30sp"
|
|
||||||
android:padding="4sp"
|
|
||||||
android:textAppearance="@style/Signal.Text.BodyMedium"
|
|
||||||
android:textColor="@color/core_white"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="1"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="1"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -1,6 +1,8 @@
|
||||||
package org.signal.core.ui
|
package org.signal.core.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
@ -132,6 +134,7 @@ object Rows {
|
||||||
icon: Painter? = null,
|
icon: Painter? = null,
|
||||||
foregroundTint: Color = MaterialTheme.colorScheme.onSurface,
|
foregroundTint: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
enabled: Boolean = true
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
TextRow(
|
TextRow(
|
||||||
|
@ -157,6 +160,7 @@ object Rows {
|
||||||
},
|
},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
enabled = enabled
|
enabled = enabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -164,18 +168,24 @@ object Rows {
|
||||||
/**
|
/**
|
||||||
* Customizable text row that allows [text] and [icon] to be provided as composable functions instead of primitives.
|
* Customizable text row that allows [text] and [icon] to be provided as composable functions instead of primitives.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TextRow(
|
fun TextRow(
|
||||||
text: @Composable RowScope.() -> Unit,
|
text: @Composable RowScope.() -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
icon: (@Composable RowScope.() -> Unit)? = null,
|
icon: (@Composable RowScope.() -> Unit)? = null,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
enabled: Boolean = true
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(enabled = enabled && onClick != null, onClick = onClick ?: {})
|
.combinedClickable(
|
||||||
|
enabled = enabled && (onClick != null || onLongClick != null),
|
||||||
|
onClick = onClick ?: {},
|
||||||
|
onLongClick = onLongClick ?: {}
|
||||||
|
)
|
||||||
.padding(defaultPadding()),
|
.padding(defaultPadding()),
|
||||||
verticalAlignment = CenterVertically
|
verticalAlignment = CenterVertically
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -37,6 +37,7 @@ dependencyResolutionManagement {
|
||||||
|
|
||||||
// Accompanist
|
// Accompanist
|
||||||
library("accompanist-permissions", "com.google.accompanist", "accompanist-permissions").versionRef("accompanist")
|
library("accompanist-permissions", "com.google.accompanist", "accompanist-permissions").versionRef("accompanist")
|
||||||
|
library("accompanist-drawablepainter", "com.google.accompanist:accompanist-drawablepainter:0.36.0")
|
||||||
|
|
||||||
// Desugaring
|
// Desugaring
|
||||||
library("android-tools-desugar", "com.android.tools:desugar_jdk_libs:1.1.6")
|
library("android-tools-desugar", "com.android.tools:desugar_jdk_libs:1.1.6")
|
||||||
|
|
|
@ -4872,6 +4872,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||||
<sha256 value="2f6174e3049008f263fd832813390df645ac5c7cfa79f170ace58690810476f2" origin="Generated by Gradle"/>
|
<sha256 value="2f6174e3049008f263fd832813390df645ac5c7cfa79f170ace58690810476f2" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="com.google.accompanist" name="accompanist-drawablepainter" version="0.36.0">
|
||||||
|
<artifact name="accompanist-drawablepainter-0.36.0.aar">
|
||||||
|
<sha256 value="599137c53f921c901ee40df056a7f940a6acdb0dd8f05763deec8cf495a40848" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
<artifact name="accompanist-drawablepainter-0.36.0.module">
|
||||||
|
<sha256 value="2d1da93fb4f071b277c47b8b0f8dfd21ee77bfb054b16a56793ff18d3841d80f" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="com.google.accompanist" name="accompanist-permissions" version="0.28.0">
|
<component group="com.google.accompanist" name="accompanist-permissions" version="0.28.0">
|
||||||
<artifact name="accompanist-permissions-0.28.0.aar">
|
<artifact name="accompanist-permissions-0.28.0.aar">
|
||||||
<sha256 value="8e0c961b18dfa0581adcc6314b00fe3b60a3aa58d2a455819fa2e82b19a806e5" origin="Generated by Gradle"/>
|
<sha256 value="8e0c961b18dfa0581adcc6314b00fe3b60a3aa58d2a455819fa2e82b19a806e5" origin="Generated by Gradle"/>
|
||||||
|
|
Loading…
Add table
Reference in a new issue