mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-03-01 22:22:15 +00:00
Rewrite App Settings in compose.
This commit is contained in:
committed by
Greyson Parrelli
parent
7f3ceea9fe
commit
b979be0cb9
@@ -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?,
|
||||
|
||||
Reference in New Issue
Block a user