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.kotlinx.collections.immutable)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
implementation(libs.kotlin.stdlib.jdk8)
|
||||
implementation(libs.kotlin.reflect)
|
||||
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.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
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.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.IdRes
|
||||
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.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavDirections
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
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.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.banners.DeprecatedBuildBanner
|
||||
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
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.banner.ui.compose.Action
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Importance
|
||||
import org.thoughtcrime.securesms.components.emoji.Emojifier
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageMedium
|
||||
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.configure
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
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.views.Stub
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class AppSettingsFragment : DSLSettingsFragment(
|
||||
titleId = R.string.text_secure_normal__menu_settings,
|
||||
layoutId = R.layout.dsl_settings_fragment_with_reminder
|
||||
) {
|
||||
class AppSettingsFragment : ComposeFragment(), Callbacks {
|
||||
|
||||
private val viewModel: AppSettingsViewModel by viewModels()
|
||||
|
||||
private var bannerManager: BannerManager? = null
|
||||
private lateinit var bannerView: Stub<ComposeView>
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewLifecycleOwner.lifecycle.addObserver(InAppPaymentsBottomSheetDelegate(childFragmentManager, viewLifecycleOwner))
|
||||
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
bannerView = ViewUtil.findStubById(view, R.id.banner_stub)
|
||||
|
||||
initializeBanners()
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(BioPreference::class.java, LayoutFactory(::BioPreferenceViewHolder, R.layout.bio_preference_item))
|
||||
adapter.registerFactory(PaymentsPreference::class.java, LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference))
|
||||
adapter.registerFactory(SubscriptionPreference::class.java, LayoutFactory(::SubscriptionPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.observeAsState()
|
||||
val self by viewModel.self.observeAsState()
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
if (state == null) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val bannerManager = remember {
|
||||
BannerManager(
|
||||
banners = listOf(
|
||||
DeprecatedBuildBanner(),
|
||||
UnauthorizedBanner(context)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
AppSettingsContent(
|
||||
self = self!!,
|
||||
state = state!!,
|
||||
bannerManager = bannerManager,
|
||||
callbacks = this
|
||||
)
|
||||
}
|
||||
|
||||
private fun initializeBanners() {
|
||||
this.bannerManager = BannerManager(
|
||||
banners = listOf(
|
||||
DeprecatedBuildBanner(),
|
||||
UnauthorizedBanner(requireContext())
|
||||
),
|
||||
onNewBannerShownListener = {
|
||||
if (bannerView.resolved()) {
|
||||
bannerView.get().addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->
|
||||
recyclerView?.setPadding(0, bottom - top, 0, 0)
|
||||
}
|
||||
recyclerView?.clipToPadding = false
|
||||
}
|
||||
},
|
||||
onNoBannerShownListener = {
|
||||
recyclerView?.clipToPadding = true
|
||||
}
|
||||
)
|
||||
override fun onNavigationClick() {
|
||||
requireActivity().finishAfterTransition()
|
||||
}
|
||||
|
||||
this.bannerManager?.updateContent(bannerView.get())
|
||||
override fun navigate(actionId: Int) {
|
||||
findNavController().safeNavigate(actionId)
|
||||
}
|
||||
|
||||
viewModel.refreshDeprecatedOrUnregistered()
|
||||
override fun navigate(directions: NavDirections) {
|
||||
findNavController().safeNavigate(directions)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshExpiredGiftBadge()
|
||||
this.bannerManager?.updateContent(bannerView.get())
|
||||
viewModel.refreshDeprecatedOrUnregistered()
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: AppSettingsState): DSLConfiguration {
|
||||
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 {
|
||||
override fun copyDonorBadgeSubscriberIdToClipboard() {
|
||||
copySubscriberIdToClipboard(
|
||||
subscriberType = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
toastSuccessStringRes = R.string.AppSettingsFragment__copied_donor_subscriber_id_to_clipboard
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun copyRemoteBackupsSubscriberIdToClipboard(): Boolean {
|
||||
override fun copyRemoteBackupsSubscriberIdToClipboard() {
|
||||
copySubscriberIdToClipboard(
|
||||
subscriberType = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
toastSuccessStringRes = R.string.AppSettingsFragment__copied_backups_subscriber_id_to_clipboard
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@Composable
|
||||
private fun AppSettingsContent(
|
||||
self: BioRecipientState,
|
||||
state: AppSettingsState,
|
||||
bannerManager: BannerManager,
|
||||
callbacks: Callbacks
|
||||
) {
|
||||
val isRegisteredAndUpToDate by rememberUpdatedState(state.isRegisteredAndUpToDate())
|
||||
|
||||
override fun areContentsTheSame(newItem: SubscriptionPreference): Boolean {
|
||||
return super.areContentsTheSame(newItem) && isActive == newItem.isActive
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
||||
private class SubscriptionPreferenceViewHolder(itemView: View) : PreferenceViewHolder<SubscriptionPreference>(itemView) {
|
||||
override fun bind(model: SubscriptionPreference) {
|
||||
super.bind(model)
|
||||
itemView.setOnClickListener { model.onClick(model.isActive) }
|
||||
itemView.setOnLongClickListener { model.onLongClick() }
|
||||
}
|
||||
}
|
||||
LazyColumn {
|
||||
item {
|
||||
BioRow(
|
||||
self = self,
|
||||
callbacks = callbacks
|
||||
)
|
||||
}
|
||||
|
||||
private class BioPreference(val recipient: Recipient, val onRowClicked: () -> Unit, val onQrButtonClicked: () -> Unit) : PreferenceModel<BioPreference>() {
|
||||
override fun areContentsTheSame(newItem: BioPreference): Boolean {
|
||||
return super.areContentsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
|
||||
}
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.AccountSettingsFragment__account),
|
||||
icon = painterResource(R.drawable.symbol_person_circle_24),
|
||||
onClick = {
|
||||
callbacks.navigate(R.id.action_appSettingsFragment_to_accountSettingsFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(newItem: BioPreference): Boolean {
|
||||
return recipient == newItem.recipient
|
||||
}
|
||||
}
|
||||
item {
|
||||
Rows.TextRow(
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
private class BioPreferenceViewHolder(itemView: View) : PreferenceViewHolder<BioPreference>(itemView) {
|
||||
item {
|
||||
val context = LocalContext.current
|
||||
val donateUrl = stringResource(R.string.donate_url)
|
||||
|
||||
private val avatarView: AvatarImageView = itemView.findViewById(R.id.icon)
|
||||
private val aboutView: EmojiTextView = itemView.findViewById(R.id.about)
|
||||
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)
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.preferences__donate_to_signal),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
init {
|
||||
aboutView.setOverflowText(" ")
|
||||
}
|
||||
if (state.hasExpiredGiftBadge) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_info_fill_24),
|
||||
tint = colorResource(R.color.signal_accent_primary),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_heart_24),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
if (state.allowUserToGoToDonationManagementScreen) {
|
||||
callbacks.navigate(R.id.action_appSettingsFragment_to_manageDonationsFragment)
|
||||
} else {
|
||||
CommunicationActions.openBrowserLink(context, donateUrl)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
callbacks.copyDonorBadgeSubscriberIdToClipboard()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun bind(model: BioPreference) {
|
||||
super.bind(model)
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
itemView.setOnClickListener { model.onRowClicked() }
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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())
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
titleView.visibility = View.VISIBLE
|
||||
summaryView.visibility = View.VISIBLE
|
||||
avatarView.visibility = View.VISIBLE
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
if (SignalStore.account.username.isNotNullOrBlank()) {
|
||||
qrButton.visibility = View.VISIBLE
|
||||
qrButton.isClickable = true
|
||||
qrButton.setOnClickListener { model.onQrButtonClicked() }
|
||||
} else {
|
||||
qrButton.visibility = View.GONE
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
if (model.recipient.combinedAboutAndEmoji != null) {
|
||||
aboutView.text = model.recipient.combinedAboutAndEmoji
|
||||
aboutView.visibility = View.VISIBLE
|
||||
} else {
|
||||
aboutView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
private class PaymentsPreference(val unreadCount: Int, val onClick: () -> Unit) : PreferenceModel<PaymentsPreference>() {
|
||||
override fun areContentsTheSame(newItem: PaymentsPreference): Boolean {
|
||||
return super.areContentsTheSame(newItem) && unreadCount == newItem.unreadCount
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(newItem: PaymentsPreference): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private class PaymentsPreferenceViewHolder(itemView: View) : MappingViewHolder<PaymentsPreference>(itemView) {
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val unreadCountView: TextView = itemView.findViewById(R.id.unread_indicator)
|
||||
if (state.showPayments) {
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
override fun bind(model: PaymentsPreference) {
|
||||
unreadCountView.text = model.unreadCount.toString()
|
||||
unreadCountView.visibility = if (model.unreadCount > 0) View.VISIBLE else View.GONE
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.preferences__payments),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
itemView.setOnClickListener {
|
||||
model.onClick()
|
||||
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 {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUsername) {
|
||||
IconButtons.IconButton(
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private interface Callbacks {
|
||||
fun onNavigationClick(): Unit = error("Not implemented.")
|
||||
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")
|
||||
}
|
||||
|
||||
private object EmptyCallbacks : Callbacks
|
||||
|
||||
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
|
||||
|
||||
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(
|
||||
val self: Recipient,
|
||||
val unreadPaymentsCount: Int,
|
||||
val hasExpiredGiftBadge: Boolean,
|
||||
val allowUserToGoToDonationManagementScreen: 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 {
|
||||
return !userUnregistered && !clientDeprecated
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.map
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
|
@ -19,7 +20,6 @@ class AppSettingsViewModel : ViewModel() {
|
|||
|
||||
private val store = Store(
|
||||
AppSettingsState(
|
||||
Recipient.self(),
|
||||
0,
|
||||
SignalStore.inAppPayments.getExpiredGiftBadge() != null,
|
||||
SignalStore.inAppPayments.isLikelyASustainer() || InAppDonations.hasAtLeastOnePaymentMethodAvailable(),
|
||||
|
@ -29,14 +29,13 @@ class AppSettingsViewModel : ViewModel() {
|
|||
)
|
||||
|
||||
private val unreadPaymentsLiveData = UnreadPaymentsLiveData()
|
||||
private val selfLiveData: LiveData<Recipient> = Recipient.self().live().liveData
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: LiveData<AppSettingsState> = store.stateLiveData
|
||||
val self: LiveData<BioRecipientState> = Recipient.self().live().liveData.map { BioRecipientState(it) }
|
||||
|
||||
init {
|
||||
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(
|
||||
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)
|
||||
}
|
||||
|
||||
@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
|
||||
fun BadgeImage112(
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
@ -132,6 +134,7 @@ object Rows {
|
|||
icon: Painter? = null,
|
||||
foregroundTint: Color = MaterialTheme.colorScheme.onSurface,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
TextRow(
|
||||
|
@ -157,6 +160,7 @@ object Rows {
|
|||
},
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
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.
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TextRow(
|
||||
text: @Composable RowScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: (@Composable RowScope.() -> Unit)? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled && onClick != null, onClick = onClick ?: {})
|
||||
.combinedClickable(
|
||||
enabled = enabled && (onClick != null || onLongClick != null),
|
||||
onClick = onClick ?: {},
|
||||
onLongClick = onLongClick ?: {}
|
||||
)
|
||||
.padding(defaultPadding()),
|
||||
verticalAlignment = CenterVertically
|
||||
) {
|
||||
|
|
|
@ -37,6 +37,7 @@ dependencyResolutionManagement {
|
|||
|
||||
// Accompanist
|
||||
library("accompanist-permissions", "com.google.accompanist", "accompanist-permissions").versionRef("accompanist")
|
||||
library("accompanist-drawablepainter", "com.google.accompanist:accompanist-drawablepainter:0.36.0")
|
||||
|
||||
// Desugaring
|
||||
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"/>
|
||||
</artifact>
|
||||
</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">
|
||||
<artifact name="accompanist-permissions-0.28.0.aar">
|
||||
<sha256 value="8e0c961b18dfa0581adcc6314b00fe3b60a3aa58d2a455819fa2e82b19a806e5" origin="Generated by Gradle"/>
|
||||
|
|
Loading…
Add table
Reference in a new issue