mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-08 17:29:02 +01:00
Add scaffolding for backupsV2.
This commit is contained in:
committed by
Nicholas Tinsley
parent
91920319c7
commit
1234c63836
@@ -497,10 +497,12 @@ dependencies {
|
||||
implementation(libs.androidx.exifinterface)
|
||||
implementation(libs.androidx.compose.rxjava3)
|
||||
implementation(libs.androidx.compose.runtime.livedata)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.multidex)
|
||||
implementation(libs.androidx.navigation.fragment.ktx)
|
||||
implementation(libs.androidx.navigation.ui.ktx)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.lifecycle.livedata.ktx)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
|
||||
@@ -757,6 +757,13 @@
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".backup.v2.ui.MessageBackupsFlowActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name=".stories.settings.StorySettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MessageBackupsCheckoutSheet(
|
||||
messageBackupsType: MessageBackupsType,
|
||||
availablePaymentGateways: List<GatewayResponse.Gateway>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dragHandle = { BottomSheets.Handle() },
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
SheetContent(
|
||||
messageBackupsType = messageBackupsType,
|
||||
availablePaymentGateways = availablePaymentGateways,
|
||||
onPaymentGatewaySelected = onPaymentGatewaySelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SheetContent(
|
||||
messageBackupsType: MessageBackupsType,
|
||||
availablePaymentGateways: List<GatewayResponse.Gateway>,
|
||||
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
val formattedPrice = remember(messageBackupsType.pricePerMonth) {
|
||||
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Pay $formattedPrice/month to Signal", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(top = 48.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "You'll get:", // TODO [message-backups] Finalized copy
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 5.dp)
|
||||
)
|
||||
|
||||
MessageBackupsTypeBlock(
|
||||
messageBackupsType = messageBackupsType,
|
||||
isSelected = false,
|
||||
onSelected = {},
|
||||
enabled = false,
|
||||
modifier = Modifier.padding(top = 24.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = spacedBy(12.dp),
|
||||
modifier = Modifier.padding(top = 48.dp, bottom = 24.dp)
|
||||
) {
|
||||
availablePaymentGateways.forEach {
|
||||
when (it) {
|
||||
GatewayResponse.Gateway.GOOGLE_PAY -> GooglePayButton {
|
||||
onPaymentGatewaySelected(GatewayResponse.Gateway.GOOGLE_PAY)
|
||||
}
|
||||
|
||||
GatewayResponse.Gateway.PAYPAL -> PayPalButton {
|
||||
onPaymentGatewaySelected(GatewayResponse.Gateway.PAYPAL)
|
||||
}
|
||||
|
||||
GatewayResponse.Gateway.CREDIT_CARD -> CreditOrDebitCardButton {
|
||||
onPaymentGatewaySelected(GatewayResponse.Gateway.CREDIT_CARD)
|
||||
}
|
||||
|
||||
GatewayResponse.Gateway.SEPA_DEBIT -> SepaButton {
|
||||
onPaymentGatewaySelected(GatewayResponse.Gateway.SEPA_DEBIT)
|
||||
}
|
||||
|
||||
GatewayResponse.Gateway.IDEAL -> IdealButton {
|
||||
onPaymentGatewaySelected(GatewayResponse.Gateway.IDEAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PayPalButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
AndroidView(factory = {
|
||||
val view = LayoutInflater.from(it).inflate(R.layout.paypal_button, null)
|
||||
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
view
|
||||
}) {
|
||||
val binding = PaypalButtonBinding.bind(it)
|
||||
binding.paypalButton.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
marginStart = 0
|
||||
marginEnd = 0
|
||||
}
|
||||
|
||||
binding.paypalButton.setOnClickListener {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GooglePayButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val model = GooglePayButton.Model(onClick, true)
|
||||
|
||||
AndroidView(factory = {
|
||||
LayoutInflater.from(it).inflate(R.layout.google_pay_button_pref, null)
|
||||
}) {
|
||||
val holder = GooglePayButton.ViewHolder(it)
|
||||
holder.bind(model)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SepaButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.bank_transfer),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(text = stringResource(id = R.string.GatewaySelectorBottomSheet__bank_transfer))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IdealButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.logo_ideal),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(text = stringResource(id = R.string.GatewaySelectorBottomSheet__ideal))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreditOrDebitCardButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Buttons.LargePrimary(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.credit_card),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.GatewaySelectorBottomSheet__credit_or_debit_card)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MessageBackupsCheckoutSheetPreview() {
|
||||
val paidTier = MessageBackupsType(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
|
||||
title = "Text + All your media",
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Full media backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "1TB of storage (~250K photos)"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
|
||||
label = "Thanks for supporting Signal!"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val availablePaymentGateways = GatewayResponse.Gateway.values().toList()
|
||||
|
||||
Previews.Preview {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
SheetContent(
|
||||
messageBackupsType = paidTier,
|
||||
availablePaymentGateways = availablePaymentGateways,
|
||||
onPaymentGatewaySelected = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Educational content which allows user to proceed to set up automatic backups
|
||||
* or navigate to a support page to learn more.
|
||||
*/
|
||||
@Composable
|
||||
fun MessageBackupsEducationScreen(
|
||||
onNavigationClick: () -> Unit,
|
||||
onEnableBackups: () -> Unit,
|
||||
onLearnMore: () -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_x_24),
|
||||
title = "Chat backups" // TODO [message-backups] Finalized copy
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
item {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Final image asset
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 48.dp)
|
||||
.size(88.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "Chat Backups", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 15.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "Back up your messages and media and using Signal’s secure, end-to-end encrypted storage service. Never lose a message when you get a new phone or reinstall Signal.", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 32.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
NotableFeatureRow(
|
||||
painter = painterResource(id = R.drawable.symbol_lock_compact_20),
|
||||
text = "End-to-end Encrypted" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
|
||||
NotableFeatureRow(
|
||||
painter = painterResource(id = R.drawable.symbol_check_square_compact_20),
|
||||
text = "Optional, always" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
|
||||
NotableFeatureRow(
|
||||
painter = painterResource(id = R.drawable.symbol_trash_compact_20),
|
||||
text = "Delete your backup anytime" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onEnableBackups,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Enable backups" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onLearnMore,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Learn more" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MessageBackupsEducationSheetPreview() {
|
||||
Previews.Preview {
|
||||
MessageBackupsEducationScreen(
|
||||
onNavigationClick = {},
|
||||
onEnableBackups = {},
|
||||
onLearnMore = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun NotableFeatureRowPreview() {
|
||||
Previews.Preview {
|
||||
NotableFeatureRow(
|
||||
painter = painterResource(id = R.drawable.symbol_lock_compact_20),
|
||||
text = "Notable feature information"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotableFeatureRow(
|
||||
painter: Painter,
|
||||
text: String
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painter,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.size(32.dp)
|
||||
.background(color = SignalTheme.colors.colorSurface2, shape = CircleShape)
|
||||
.padding(6.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.dialog
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
|
||||
|
||||
private val viewModel: MessageBackupsFlowViewModel by viewModel { MessageBackupsFlowViewModel() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
setContent {
|
||||
SignalTheme {
|
||||
val state by viewModel.state
|
||||
val navController = rememberNavController()
|
||||
|
||||
fun MessageBackupsScreen.next() {
|
||||
val nextScreen = viewModel.goToNextScreen(this)
|
||||
if (nextScreen != this) {
|
||||
navController.navigate(nextScreen.name)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.popOrFinish() {
|
||||
if (popBackStack()) {
|
||||
return
|
||||
}
|
||||
|
||||
finishAfterTransition()
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
navController.setLifecycleOwner(this@MessageBackupsFlowActivity)
|
||||
navController.setOnBackPressedDispatcher(this@MessageBackupsFlowActivity.onBackPressedDispatcher)
|
||||
navController.enableOnBackPressed(true)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = MessageBackupsScreen.EDUCATION.name,
|
||||
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
|
||||
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
|
||||
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
|
||||
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
|
||||
) {
|
||||
composable(route = MessageBackupsScreen.EDUCATION.name) {
|
||||
MessageBackupsEducationScreen(
|
||||
onNavigationClick = navController::popOrFinish,
|
||||
onEnableBackups = { MessageBackupsScreen.EDUCATION.next() },
|
||||
onLearnMore = {}
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = MessageBackupsScreen.PIN_EDUCATION.name) {
|
||||
MessageBackupsPinEducationScreen(
|
||||
onNavigationClick = navController::popOrFinish,
|
||||
onGeneratePinClick = {},
|
||||
onUseCurrentPinClick = { MessageBackupsScreen.PIN_EDUCATION.next() },
|
||||
recommendedPinSize = 16 // TODO [message-backups] This value should come from some kind of config
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = MessageBackupsScreen.PIN_CONFIRMATION.name) {
|
||||
MessageBackupsPinConfirmationScreen(
|
||||
pin = state.pin,
|
||||
onPinChanged = viewModel::onPinEntryUpdated,
|
||||
pinKeyboardType = state.pinKeyboardType,
|
||||
onPinKeyboardTypeSelected = viewModel::onPinKeyboardTypeUpdated,
|
||||
onNextClick = { MessageBackupsScreen.PIN_CONFIRMATION.next() }
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
|
||||
MessageBackupsTypeSelectionScreen(
|
||||
selectedBackupsType = state.selectedMessageBackupsType,
|
||||
availableBackupsTypes = state.availableBackupsTypes,
|
||||
onMessageBackupsTypeSelected = viewModel::onMessageBackupsTypeUpdated,
|
||||
onNavigationClick = navController::popOrFinish,
|
||||
onReadMoreClicked = {},
|
||||
onNextClicked = { MessageBackupsScreen.TYPE_SELECTION.next() }
|
||||
)
|
||||
}
|
||||
|
||||
dialog(route = MessageBackupsScreen.CHECKOUT_SHEET.name) {
|
||||
MessageBackupsCheckoutSheet(
|
||||
messageBackupsType = state.selectedMessageBackupsType!!,
|
||||
availablePaymentGateways = state.availablePaymentGateways,
|
||||
onDismissRequest = navController::popOrFinish,
|
||||
onPaymentGatewaySelected = {
|
||||
viewModel.onPaymentGatewayUpdated(it)
|
||||
MessageBackupsScreen.CHECKOUT_SHEET.next()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
class MessageBackupsFlowRepository
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
|
||||
data class MessageBackupsFlowState(
|
||||
val selectedMessageBackupsType: MessageBackupsType? = null,
|
||||
val availableBackupsTypes: List<MessageBackupsType> = emptyList(),
|
||||
val selectedPaymentGateway: GatewayResponse.Gateway? = null,
|
||||
val availablePaymentGateways: List<GatewayResponse.Gateway> = emptyList(),
|
||||
val pin: String = "",
|
||||
val pinKeyboardType: PinKeyboardType = SignalStore.pinValues().keyboardType
|
||||
)
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
|
||||
class MessageBackupsFlowViewModel : ViewModel() {
|
||||
private val internalState = mutableStateOf(MessageBackupsFlowState())
|
||||
|
||||
val state: State<MessageBackupsFlowState> = internalState
|
||||
|
||||
fun goToNextScreen(currentScreen: MessageBackupsScreen): MessageBackupsScreen {
|
||||
return when (currentScreen) {
|
||||
MessageBackupsScreen.EDUCATION -> MessageBackupsScreen.PIN_EDUCATION
|
||||
MessageBackupsScreen.PIN_EDUCATION -> MessageBackupsScreen.PIN_CONFIRMATION
|
||||
MessageBackupsScreen.PIN_CONFIRMATION -> validatePinAndUpdateState()
|
||||
MessageBackupsScreen.TYPE_SELECTION -> validateTypeAndUpdateState()
|
||||
MessageBackupsScreen.CHECKOUT_SHEET -> validateGatewayAndUpdateState()
|
||||
MessageBackupsScreen.PROCESS_PAYMENT -> MessageBackupsScreen.COMPLETED
|
||||
MessageBackupsScreen.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
|
||||
}
|
||||
}
|
||||
|
||||
fun onPinEntryUpdated(pin: String) {
|
||||
internalState.value = state.value.copy(pin = pin)
|
||||
}
|
||||
|
||||
fun onPinKeyboardTypeUpdated(pinKeyboardType: PinKeyboardType) {
|
||||
internalState.value = state.value.copy(pinKeyboardType = pinKeyboardType)
|
||||
}
|
||||
|
||||
fun onPaymentGatewayUpdated(gateway: GatewayResponse.Gateway) {
|
||||
internalState.value = state.value.copy(selectedPaymentGateway = gateway)
|
||||
}
|
||||
|
||||
fun onMessageBackupsTypeUpdated(messageBackupsType: MessageBackupsType) {
|
||||
internalState.value = state.value.copy(selectedMessageBackupsType = messageBackupsType)
|
||||
}
|
||||
|
||||
private fun validatePinAndUpdateState(): MessageBackupsScreen {
|
||||
return MessageBackupsScreen.TYPE_SELECTION
|
||||
}
|
||||
|
||||
private fun validateTypeAndUpdateState(): MessageBackupsScreen {
|
||||
return MessageBackupsScreen.CHECKOUT_SHEET
|
||||
}
|
||||
|
||||
private fun validateGatewayAndUpdateState(): MessageBackupsScreen {
|
||||
return MessageBackupsScreen.PROCESS_PAYMENT
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
|
||||
/**
|
||||
* Screen which requires the user to enter their pin before enabling backups.
|
||||
*/
|
||||
@Composable
|
||||
fun MessageBackupsPinConfirmationScreen(
|
||||
pin: String,
|
||||
onPinChanged: (String) -> Unit,
|
||||
pinKeyboardType: PinKeyboardType,
|
||||
onPinKeyboardTypeSelected: (PinKeyboardType) -> Unit,
|
||||
onNextClick: () -> Unit
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = "Enter your PIN", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 40.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "Enter your Signal PIN to enable backups", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
// TODO [message-backups] Confirm default focus state
|
||||
val keyboardType = remember(pinKeyboardType) {
|
||||
when (pinKeyboardType) {
|
||||
PinKeyboardType.NUMERIC -> KeyboardType.NumberPassword
|
||||
PinKeyboardType.ALPHA_NUMERIC -> KeyboardType.Password
|
||||
}
|
||||
}
|
||||
|
||||
TextField(
|
||||
value = pin,
|
||||
onValueChange = onPinChanged,
|
||||
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { onNextClick() }
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = keyboardType
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(top = 72.dp)
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 48.dp)
|
||||
) {
|
||||
PinKeyboardTypeToggle(
|
||||
pinKeyboardType = pinKeyboardType,
|
||||
onPinKeyboardTypeSelected = onPinKeyboardTypeSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.BottomEnd,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onNextClick
|
||||
) {
|
||||
Text(
|
||||
text = "Next" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MessageBackupsPinConfirmationScreenPreview() {
|
||||
Previews.Preview {
|
||||
MessageBackupsPinConfirmationScreen(
|
||||
pin = "",
|
||||
onPinChanged = {},
|
||||
pinKeyboardType = PinKeyboardType.ALPHA_NUMERIC,
|
||||
onPinKeyboardTypeSelected = {},
|
||||
onNextClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PinKeyboardTypeTogglePreview() {
|
||||
Previews.Preview {
|
||||
var type by remember { mutableStateOf(PinKeyboardType.ALPHA_NUMERIC) }
|
||||
PinKeyboardTypeToggle(
|
||||
pinKeyboardType = type,
|
||||
onPinKeyboardTypeSelected = { type = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinKeyboardTypeToggle(
|
||||
pinKeyboardType: PinKeyboardType,
|
||||
onPinKeyboardTypeSelected: (PinKeyboardType) -> Unit
|
||||
) {
|
||||
val callback = remember(pinKeyboardType) {
|
||||
{ onPinKeyboardTypeSelected(pinKeyboardType.other) }
|
||||
}
|
||||
|
||||
val iconRes = remember(pinKeyboardType) {
|
||||
when (pinKeyboardType) {
|
||||
PinKeyboardType.NUMERIC -> R.drawable.symbol_keyboard_24
|
||||
PinKeyboardType.ALPHA_NUMERIC -> R.drawable.symbol_number_pad_24
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(onClick = callback) {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Switch keyboard" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Explanation screen that details how the user's pin is utilized with backups,
|
||||
* and how long they should make their pin.
|
||||
*/
|
||||
@Composable
|
||||
fun MessageBackupsPinEducationScreen(
|
||||
onNavigationClick: () -> Unit,
|
||||
onGeneratePinClick: () -> Unit,
|
||||
onUseCurrentPinClick: () -> Unit,
|
||||
recommendedPinSize: Int
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "Backup type", // TODO [message-backups] Finalized copy
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
item {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Finalized image
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 48.dp)
|
||||
.size(88.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "PINs protect your backup", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "Your Signal PIN lets you restore your backup when you re-install Signal. For increased security, we recommend updating to a new $recommendedPinSize-digit PIN.", // TODO [message-backups] Finalized copy
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "If you forget your PIN, you will not be able to restore your backup. You can change your PIN at any time in settings.", // TODO [message-backups] Finalized copy
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onGeneratePinClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Generate a new $recommendedPinSize-digit PIN" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onUseCurrentPinClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Use current Signal PIN" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MessageBackupsPinScreenPreview() {
|
||||
Previews.Preview {
|
||||
MessageBackupsPinEducationScreen(
|
||||
onNavigationClick = {},
|
||||
onGeneratePinClick = {},
|
||||
onUseCurrentPinClick = {},
|
||||
recommendedPinSize = 16
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
enum class MessageBackupsScreen {
|
||||
EDUCATION,
|
||||
PIN_EDUCATION,
|
||||
PIN_CONFIRMATION,
|
||||
TYPE_SELECTION,
|
||||
CHECKOUT_SHEET,
|
||||
PROCESS_PAYMENT,
|
||||
COMPLETED
|
||||
}
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withAnnotation
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
/**
|
||||
* Screen which allows the user to select their preferred backup type.
|
||||
*/
|
||||
@OptIn(ExperimentalTextApi::class)
|
||||
@Composable
|
||||
fun MessageBackupsTypeSelectionScreen(
|
||||
selectedBackupsType: MessageBackupsType?,
|
||||
availableBackupsTypes: List<MessageBackupsType>,
|
||||
onMessageBackupsTypeSelected: (MessageBackupsType) -> Unit,
|
||||
onNavigationClick: () -> Unit,
|
||||
onReadMoreClicked: () -> Unit,
|
||||
onNextClicked: () -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "",
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
item {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Finalized art asset
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(88.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "Choose your backup type", // TODO [message-backups] Finalized copy
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
// TODO [message-backups] Finalized copy
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val readMoreString = buildAnnotatedString {
|
||||
append("All backups are end-to-end encrypted. Signal is a non-profit—paying for backups helps support our mission. ")
|
||||
withAnnotation(tag = "URL", annotation = "read-more") {
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = primaryColor
|
||||
)
|
||||
) {
|
||||
append("Read more")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = readMoreString,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center),
|
||||
onClick = { offset ->
|
||||
readMoreString
|
||||
.getStringAnnotations(tag = "URL", start = offset, end = offset)
|
||||
.firstOrNull()?.let { onReadMoreClicked() }
|
||||
},
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
availableBackupsTypes,
|
||||
{ _, item -> item.title }
|
||||
) { index, item ->
|
||||
MessageBackupsTypeBlock(
|
||||
messageBackupsType = item,
|
||||
isSelected = item == selectedBackupsType,
|
||||
onSelected = { onMessageBackupsTypeSelected(item) },
|
||||
modifier = Modifier.padding(top = if (index == 0) 20.dp else 18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onNextClicked,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Next" // TODO [message-backups] Finalized copy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MessageBackupsTypeSelectionScreenPreview() {
|
||||
val freeTier = MessageBackupsType(
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
|
||||
title = "Text + 30 days of media",
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Last 30 days of media"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val paidTier = MessageBackupsType(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
|
||||
title = "Text + All your media",
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Full media backup"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "1TB of storage (~250K photos)"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
|
||||
label = "Thanks for supporting Signal!"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
var selectedBackupsType by remember { mutableStateOf(freeTier) }
|
||||
|
||||
Previews.Preview {
|
||||
MessageBackupsTypeSelectionScreen(
|
||||
selectedBackupsType = selectedBackupsType,
|
||||
availableBackupsTypes = listOf(freeTier, paidTier),
|
||||
onMessageBackupsTypeSelected = { selectedBackupsType = it },
|
||||
onNavigationClick = {},
|
||||
onReadMoreClicked = {},
|
||||
onNextClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageBackupsTypeBlock(
|
||||
messageBackupsType: MessageBackupsType,
|
||||
isSelected: Boolean,
|
||||
onSelected: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
val borderColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
Color.Transparent
|
||||
}
|
||||
|
||||
val background = if (isSelected) {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
} else {
|
||||
SignalTheme.colors.colorSurface2
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = background, shape = RoundedCornerShape(18.dp))
|
||||
.border(width = 2.dp, color = borderColor, shape = RoundedCornerShape(18.dp))
|
||||
.clip(shape = RoundedCornerShape(18.dp))
|
||||
.clickable(onClick = onSelected, enabled = enabled)
|
||||
.padding(vertical = 16.dp, horizontal = 20.dp)
|
||||
) {
|
||||
Text(
|
||||
text = formatCostPerMonth(messageBackupsType.pricePerMonth),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
|
||||
Text(
|
||||
text = messageBackupsType.title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
messageBackupsType.features.forEach {
|
||||
MessageBackupsTypeFeatureRow(messageBackupsTypeFeature = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
|
||||
return if (pricePerMonth.amount == BigDecimal.ZERO) {
|
||||
"Free"
|
||||
} else {
|
||||
"${FiatMoneyUtil.format(LocalContext.current.resources, pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())}/month"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBackupsTypeFeatureRow(messageBackupsTypeFeature: MessageBackupsTypeFeature) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = messageBackupsTypeFeature.iconResourceId),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = messageBackupsTypeFeature.label,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class MessageBackupsType(
|
||||
val pricePerMonth: FiatMoney,
|
||||
val title: String,
|
||||
val features: ImmutableList<MessageBackupsTypeFeature>
|
||||
)
|
||||
|
||||
data class MessageBackupsTypeFeature(
|
||||
val iconResourceId: Int,
|
||||
val label: String
|
||||
)
|
||||
-1
@@ -196,7 +196,6 @@ import java.util.stream.Collectors;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
import static android.app.Activity.RESULT_CANCELED;
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,15 @@ class SavedStateViewModelFactory<MODEL : ViewModel>(
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> ComponentActivity.viewModel(
|
||||
noinline create: () -> VM
|
||||
): Lazy<VM> {
|
||||
return viewModels(
|
||||
factoryProducer = ViewModelFactory.factoryProducer(create)
|
||||
)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> Fragment.viewModel(
|
||||
noinline create: () -> VM
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6.38 7.88c0-0.63 0.5-1.13 1.12-1.13 0.62 0 1.13 0.5 1.13 1.13C8.63 8.5 8.13 9 7.5 9 6.88 9 6.37 8.5 6.37 7.87Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4.67 1.75c-0.53 0-0.98 0-1.34 0.03-0.38 0.03-0.74 0.1-1.08 0.27-0.52 0.26-0.94 0.68-1.2 1.2-0.17 0.34-0.24 0.7-0.27 1.08-0.03 0.36-0.03 0.8-0.03 1.34v1.86c0 0.44 0 0.81 0.02 1.12 0.02 0.31 0.07 0.61 0.19 0.9 0.28 0.68 0.81 1.21 1.49 1.5 0.29 0.11 0.59 0.16 0.9 0.18l0.4 0.02 0.03 0.42c0.03 0.38 0.1 0.74 0.27 1.08 0.26 0.52 0.68 0.94 1.2 1.2 0.34 0.17 0.7 0.24 1.08 0.27 0.36 0.03 0.8 0.03 1.34 0.03h3.66c0.53 0 0.98 0 1.34-0.03 0.38-0.03 0.74-0.1 1.08-0.27 0.52-0.26 0.94-0.68 1.2-1.2 0.17-0.34 0.24-0.7 0.27-1.08 0.03-0.36 0.03-0.8 0.03-1.34V8.67c0-0.53 0-0.98-0.03-1.34-0.03-0.38-0.1-0.74-0.27-1.08-0.26-0.52-0.68-0.94-1.2-1.2-0.34-0.17-0.7-0.24-1.08-0.27l-0.42-0.02-0.02-0.4c-0.02-0.32-0.07-0.62-0.19-0.91-0.28-0.68-0.81-1.21-1.49-1.5-0.29-0.11-0.59-0.16-0.9-0.18-0.3-0.02-0.68-0.02-1.12-0.02H4.67Zm6.08 3H7.67c-0.53 0-0.98 0-1.34 0.03-0.38 0.03-0.74 0.1-1.08 0.27-0.52 0.26-0.94 0.68-1.2 1.2-0.17 0.34-0.24 0.7-0.27 1.08-0.03 0.36-0.03 0.8-0.03 1.34v1.08l-0.3-0.02C3.23 9.72 3.1 9.7 3.03 9.65c-0.3-0.12-0.55-0.37-0.67-0.67C2.3 8.9 2.28 8.78 2.27 8.54 2.25 8.3 2.25 7.98 2.25 7.5V5.7c0-0.57 0-0.96 0.02-1.25C2.3 4.16 2.34 4.02 2.4 3.93 2.5 3.7 2.69 3.51 2.93 3.4c0.1-0.05 0.23-0.1 0.52-0.12 0.3-0.02 0.68-0.02 1.25-0.02h3.8c0.48 0 0.8 0 1.04 0.02C9.78 3.28 9.9 3.3 9.98 3.35c0.3 0.12 0.55 0.37 0.67 0.67 0.04 0.08 0.07 0.2 0.08 0.44l0.02 0.29ZM5.93 6.39c0.1-0.05 0.23-0.1 0.52-0.12 0.3-0.02 0.68-0.02 1.25-0.02h3.6c0.57 0 0.96 0 1.25 0.02 0.29 0.03 0.43 0.07 0.52 0.12 0.23 0.12 0.42 0.3 0.54 0.54 0.05 0.1 0.1 0.23 0.12 0.52 0.02 0.3 0.02 0.68 0.02 1.25v0.74l-0.76-0.76c-0.69-0.69-1.8-0.69-2.48 0L9 10.18 8.74 9.94c-0.69-0.69-1.8-0.69-2.48 0l-1 1-0.01-0.63V8.7c0-0.57 0-0.96 0.02-1.25C5.3 7.16 5.34 7.02 5.4 6.93 5.5 6.7 5.69 6.51 5.93 6.4Zm6 3.35l1.8 1.8v0.01c-0.03 0.29-0.07 0.43-0.12 0.52-0.12 0.23-0.3 0.42-0.54 0.54-0.1 0.05-0.23 0.1-0.52 0.12-0.3 0.02-0.68 0.02-1.25 0.02H7.7c-0.57 0-0.96 0-1.25-0.02-0.29-0.03-0.43-0.07-0.52-0.12L5.8 12.53l1.53-1.54c0.1-0.1 0.26-0.1 0.36 0l0.79 0.79c0.3 0.3 0.77 0.3 1.06 0l2.04-2.04c0.1-0.1 0.26-0.1 0.36 0Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.92 5.98c0.2-0.3 0.11-0.7-0.2-0.9-0.3-0.2-0.7-0.11-0.89 0.2L7.08 9.5l-1.2-1.53c-0.21-0.29-0.62-0.34-0.9-0.12C4.69 8.08 4.64 8.5 4.86 8.77l1.75 2.25c0.13 0.17 0.33 0.26 0.54 0.25 0.21 0 0.4-0.11 0.52-0.3l3.25-5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6.77 1.35c-0.81 0-1.47 0-2 0.04-0.53 0.05-1 0.14-1.43 0.36-0.68 0.35-1.24 0.9-1.6 1.6-0.21 0.42-0.3 0.89-0.35 1.43-0.04 0.52-0.04 1.18-0.04 2v2.45c0 0.81 0 1.47 0.04 2 0.05 0.53 0.14 1 0.36 1.43 0.35 0.68 0.9 1.24 1.6 1.6 0.42 0.21 0.89 0.3 1.43 0.35 0.52 0.04 1.18 0.04 2 0.04h2.45c0.81 0 1.47 0 2-0.04 0.53-0.05 1-0.14 1.43-0.36 0.68-0.35 1.24-0.9 1.6-1.6 0.21-0.42 0.3-0.89 0.35-1.43 0.04-0.52 0.04-1.18 0.04-2V6.78c0-0.81 0-1.47-0.04-2-0.05-0.53-0.14-1-0.36-1.43-0.35-0.68-0.9-1.24-1.6-1.6-0.42-0.21-0.89-0.3-1.43-0.35-0.52-0.04-1.18-0.04-2-0.04H6.78ZM3.93 2.91c0.22-0.11 0.5-0.18 0.95-0.22 0.47-0.04 1.07-0.04 1.92-0.04h2.4c0.85 0 1.45 0 1.92 0.04 0.46 0.04 0.73 0.1 0.95 0.22 0.44 0.22 0.8 0.58 1.02 1.02 0.11 0.22 0.18 0.5 0.22 0.95 0.04 0.47 0.04 1.07 0.04 1.92v2.4c0 0.85 0 1.45-0.04 1.92-0.04 0.46-0.1 0.73-0.22 0.95-0.22 0.44-0.58 0.8-1.02 1.02-0.22 0.11-0.5 0.18-0.95 0.22-0.47 0.04-1.07 0.04-1.92 0.04H6.8c-0.85 0-1.45 0-1.92-0.04-0.46-0.04-0.73-0.1-0.95-0.22-0.44-0.22-0.8-0.58-1.02-1.02-0.11-0.22-0.18-0.5-0.22-0.95-0.04-0.47-0.04-1.07-0.04-1.92V6.8c0-0.85 0-1.45 0.04-1.92 0.04-0.46 0.1-0.73 0.22-0.95 0.22-0.44 0.58-0.8 1.02-1.02Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12 1c6.08 0 11 4.92 11 11s-4.92 11-11 11S1 18.08 1 12 5.92 1 12 1Zm-1 2.05C6.5 3.55 3 7.37 3 12c0 4.97 4.03 9 9 9 1.78 0 3.44-0.52 4.84-1.4l-5.18-6.42c-0.43-0.54-0.66-1.2-0.66-1.89V3.05Zm7.4 15.28c1.41-1.43 2.35-3.33 2.56-5.45H16l-2.12-0.14 4.51 5.6ZM13 10.88h7.93C20.42 6.75 17.13 3.5 13 3.04v7.83Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M1.95 3.03C2.7 2.2 3.77 1.75 4.9 1.75c1.23 0 2.34 0.62 3.09 1.54 0.75-0.92 1.86-1.54 3.09-1.54 1.14 0 2.2 0.45 2.96 1.28 0.77 0.82 1.2 1.99 1.2 3.36 0 1.54-0.85 3.3-2.07 4.84-1.24 1.56-2.95 3.02-4.85 3.94-0.2 0.1-0.45 0.1-0.66 0-1.9-0.92-3.6-2.38-4.85-3.94-1.22-1.55-2.07-3.3-2.07-4.84 0-1.37 0.43-2.54 1.2-3.36Zm1.1 1.01c-0.47 0.51-0.8 1.3-0.8 2.35S2.86 8.87 4 10.3c1.04 1.32 2.45 2.55 4 3.37 1.55-0.82 2.96-2.05 4-3.36 1.14-1.43 1.75-2.86 1.75-3.9 0-1.07-0.33-1.85-0.8-2.36-0.47-0.5-1.12-0.79-1.86-0.79-1 0-2 0.73-2.39 1.78C8.6 5.33 8.31 5.52 8 5.52c-0.31 0-0.6-0.2-0.7-0.49C6.9 3.98 5.92 3.25 4.9 3.25c-0.74 0-1.39 0.28-1.86 0.8Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,42 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12 9.75c0.69 0 1.25-0.56 1.25-1.25S12.69 7.25 12 7.25s-1.25 0.56-1.25 1.25 0.56 1.25 1.25 1.25Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13.25 12c0 0.69-0.56 1.25-1.25 1.25s-1.25-0.56-1.25-1.25 0.56-1.25 1.25-1.25 1.25 0.56 1.25 1.25Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8.5 9.75c0.69 0 1.25-0.56 1.25-1.25S9.19 7.25 8.5 7.25 7.25 7.81 7.25 8.5 7.81 9.75 8.5 9.75Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9.75 12c0 0.69-0.56 1.25-1.25 1.25S7.25 12.69 7.25 12s0.56-1.25 1.25-1.25S9.75 11.31 9.75 12Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.5 9.75c0.69 0 1.25-0.56 1.25-1.25s-0.56-1.25-1.25-1.25-1.25 0.56-1.25 1.25 0.56 1.25 1.25 1.25Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16.75 12c0 0.69-0.56 1.25-1.25 1.25s-1.25-0.56-1.25-1.25 0.56-1.25 1.25-1.25 1.25 0.56 1.25 1.25Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19 9.75c0.69 0 1.25-0.56 1.25-1.25S19.69 7.25 19 7.25s-1.25 0.56-1.25 1.25 0.56 1.25 1.25 1.25Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20.25 12c0 0.69-0.56 1.25-1.25 1.25s-1.25-0.56-1.25-1.25 0.56-1.25 1.25-1.25 1.25 0.56 1.25 1.25Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M5 9.75c0.69 0 1.25-0.56 1.25-1.25S5.69 7.25 5 7.25 3.75 7.81 3.75 8.5 4.31 9.75 5 9.75Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6.25 12c0 0.69-0.56 1.25-1.25 1.25S3.75 12.69 3.75 12 4.31 10.75 5 10.75 6.25 11.31 6.25 12Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8.25 15c-0.55 0-1 0.45-1 1s0.45 1 1 1h7.5c0.55 0 1-0.45 1-1s-0.45-1-1-1h-7.5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6.26 3.63c-0.8 0-1.47 0-2 0.04C3.7 3.7 3.2 3.8 2.74 4.05 2.01 4.42 1.42 5 1.04 5.75 0.82 6.2 0.72 6.7 0.68 7.24 0.62 7.8 0.62 8.46 0.62 9.27v5.48c0 0.8 0 1.47 0.05 2C0.7 17.3 0.8 17.8 1.05 18.26c0.37 0.73 0.96 1.32 1.7 1.7 0.46 0.23 0.95 0.33 1.5 0.37 0.54 0.05 1.2 0.05 2.01 0.05h11.48c0.8 0 1.47 0 2-0.05 0.56-0.04 1.05-0.14 1.52-0.38 0.73-0.37 1.32-0.96 1.7-1.7 0.23-0.46 0.33-0.95 0.37-1.5 0.05-0.54 0.05-1.2 0.05-2.01V9.26c0-0.8 0-1.47-0.05-2C23.3 6.7 23.2 6.2 22.95 5.74c-0.37-0.73-0.96-1.32-1.7-1.7-0.46-0.23-0.95-0.33-1.5-0.37-0.54-0.05-1.2-0.05-2.01-0.04H6.26ZM3.54 5.6C3.7 5.5 3.95 5.45 4.4 5.4c0.46-0.03 1.05-0.04 1.9-0.04h11.4c0.85 0 1.44 0 1.9 0.04 0.45 0.04 0.69 0.1 0.86 0.2 0.4 0.2 0.73 0.53 0.93 0.93 0.1 0.17 0.16 0.41 0.2 0.86 0.03 0.46 0.04 1.05 0.04 1.9v5.4c0 0.85 0 1.44-0.04 1.9-0.04 0.45-0.1 0.69-0.2 0.86-0.2 0.4-0.53 0.73-0.93 0.93-0.17 0.1-0.41 0.16-0.86 0.2-0.46 0.03-1.05 0.04-1.9 0.04H6.3c-0.85 0-1.44 0-1.9-0.04-0.45-0.04-0.69-0.1-0.86-0.2-0.4-0.2-0.73-0.53-0.93-0.93-0.1-0.17-0.16-0.41-0.2-0.86-0.03-0.46-0.04-1.05-0.04-1.9V9.3c0-0.85 0-1.44 0.04-1.9 0.04-0.45 0.1-0.69 0.2-0.86 0.2-0.4 0.53-0.73 0.93-0.93Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M7.35 10.42c-0.23-0.2-0.37-0.48-0.37-0.8C6.98 9.06 7.43 8.6 8 8.6c0.57 0 1.03 0.46 1.03 1.03 0 0.31-0.15 0.6-0.38 0.79v1.08c0 0.36-0.3 0.65-0.65 0.65-0.36 0-0.65-0.3-0.65-0.65v-1.08Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M3.85 4.5v1.64C3.43 6.37 3.08 6.73 2.86 7.16c-0.15 0.3-0.2 0.6-0.23 0.94C2.6 8.42 2.6 8.8 2.6 9.27v2.46c0 0.46 0 0.85 0.03 1.17 0.02 0.33 0.08 0.65 0.23 0.94 0.23 0.45 0.6 0.82 1.05 1.05 0.3 0.15 0.6 0.2 0.94 0.23 0.32 0.03 0.7 0.03 1.17 0.03h3.96c0.46 0 0.85 0 1.17-0.03 0.33-0.02 0.65-0.08 0.94-0.23 0.45-0.23 0.82-0.6 1.05-1.05 0.15-0.3 0.2-0.6 0.23-0.94 0.03-0.32 0.03-0.7 0.03-1.17V9.27c0-0.46 0-0.85-0.03-1.17-0.02-0.33-0.08-0.65-0.23-0.94-0.22-0.43-0.57-0.79-0.99-1.02V4.5c0-2.3-1.86-4.15-4.15-4.15-2.3 0-4.15 1.86-4.15 4.15Zm7 0v1.36L9.98 5.85H6.02 5.15V4.5c0-1.57 1.28-2.85 2.85-2.85s2.85 1.28 2.85 2.85ZM4.5 7.27c0.08-0.04 0.2-0.08 0.45-0.1 0.26-0.02 0.6-0.02 1.1-0.02h3.9c0.5 0 0.84 0 1.1 0.02 0.25 0.02 0.37 0.06 0.45 0.1 0.2 0.1 0.37 0.27 0.48 0.48 0.04 0.08 0.08 0.2 0.1 0.45 0.02 0.26 0.02 0.6 0.02 1.1v2.4c0 0.5 0 0.84-0.02 1.1-0.02 0.25-0.06 0.37-0.1 0.45-0.1 0.2-0.27 0.37-0.48 0.48-0.08 0.04-0.2 0.08-0.45 0.1-0.26 0.02-0.6 0.02-1.1 0.02h-3.9c-0.5 0-0.84 0-1.1-0.02-0.25-0.02-0.37-0.06-0.45-0.1-0.2-0.1-0.37-0.27-0.48-0.48-0.04-0.08-0.08-0.2-0.1-0.45-0.02-0.26-0.02-0.6-0.02-1.1V9.3c0-0.5 0-0.84 0.02-1.1 0.02-0.25 0.06-0.37 0.1-0.45 0.1-0.2 0.27-0.37 0.48-0.48Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,36 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8.38 4.75c0 0.9-0.73 1.63-1.63 1.63-0.9 0-1.63-0.73-1.63-1.63 0-0.9 0.73-1.63 1.63-1.63 0.9 0 1.63 0.73 1.63 1.63Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12 6.38c0.9 0 1.63-0.73 1.63-1.63 0-0.9-0.73-1.63-1.63-1.63-0.9 0-1.63 0.73-1.63 1.63 0 0.9 0.73 1.63 1.63 1.63Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17.25 6.38c0.9 0 1.63-0.73 1.63-1.63 0-0.9-0.73-1.63-1.63-1.63-0.9 0-1.63 0.73-1.63 1.63 0 0.9 0.73 1.63 1.63 1.63Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12 21.38c0.9 0 1.63-0.73 1.63-1.63 0-0.9-0.73-1.63-1.63-1.63-0.9 0-1.63 0.73-1.63 1.63 0 0.9 0.73 1.63 1.63 1.63Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13.63 14.75c0 0.9-0.73 1.63-1.63 1.63-0.9 0-1.63-0.73-1.63-1.63 0-0.9 0.73-1.63 1.63-1.63 0.9 0 1.63 0.73 1.63 1.63Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17.25 16.38c0.9 0 1.63-0.73 1.63-1.63 0-0.9-0.73-1.63-1.63-1.63-0.9 0-1.63 0.73-1.63 1.63 0 0.9 0.73 1.63 1.63 1.63Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8.38 14.75c0 0.9-0.73 1.63-1.63 1.63-0.9 0-1.63-0.73-1.63-1.63 0-0.9 0.73-1.63 1.63-1.63 0.9 0 1.63 0.73 1.63 1.63Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12 11.38c0.9 0 1.63-0.73 1.63-1.63 0-0.9-0.73-1.63-1.63-1.63-0.9 0-1.63 0.73-1.63 1.63 0 0.9 0.73 1.63 1.63 1.63Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18.88 9.75c0 0.9-0.73 1.63-1.63 1.63-0.9 0-1.63-0.73-1.63-1.63 0-0.9 0.73-1.63 1.63-1.63 0.9 0 1.63 0.73 1.63 1.63Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6.75 11.38c0.9 0 1.63-0.73 1.63-1.63 0-0.9-0.73-1.63-1.63-1.63-0.9 0-1.63 0.73-1.63 1.63 0 0.9 0.73 1.63 1.63 1.63Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<group>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9.15 8.25c0 0.5-0.4 0.9-0.9 0.9h-0.1L4.7 8.9C4.36 8.87 4.1 8.6 4.1 8.25c0-0.34 0.26-0.62 0.6-0.65l2.7-0.2 0.2-3.69C7.62 3.37 7.9 3.1 8.25 3.1c0.34 0 0.63 0.27 0.65 0.61l0.25 4.46v0.08Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8 0.75C4 0.75 0.75 4 0.75 8S4 15.25 8 15.25 15.25 12 15.25 8 12 0.75 8 0.75ZM2.25 8c0-3.18 2.57-5.75 5.75-5.75S13.75 4.82 13.75 8 11.18 13.75 8 13.75 2.25 11.18 2.25 8Z"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M5.33 2.42c-1.8 0-3.25 1.45-3.25 3.25 0 0.8 0.29 1.52 0.77 2.09 0.17 0.21 0.28 0.5 0.25 0.8l-0.08 1L3.8 9.01c0.25-0.18 0.55-0.23 0.82-0.18V9c0 0.48 0.05 0.94 0.16 1.38-0.1 0-0.18-0.02-0.27-0.03l-1.36 0.94c-0.75 0.52-1.78-0.07-1.7-0.98L1.6 8.6C0.96 7.79 0.58 6.77 0.58 5.67c0-2.63 2.13-4.75 4.75-4.75 1.68 0 3.15 0.87 4 2.18-0.51 0.11-1 0.3-1.46 0.53-0.6-0.74-1.51-1.21-2.54-1.21Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.67 4.25c2.62 0 4.75 2.13 4.75 4.75 0 1.1-0.38 2.12-1 2.92l0.14 1.77c0.08 0.91-0.94 1.5-1.7 0.98l-1.41-0.98c-0.26 0.04-0.52 0.06-0.78 0.06-2.63 0-4.75-2.13-4.75-4.75s2.12-4.75 4.75-4.75ZM13.92 9c0-1.8-1.46-3.25-3.25-3.25-1.8 0-3.25 1.46-3.25 3.25 0 1.8 1.45 3.25 3.25 3.25 0.22 0 0.44-0.02 0.65-0.07 0.28-0.05 0.59 0 0.85 0.18l0.82 0.57-0.09-1.05c-0.02-0.3 0.08-0.58 0.26-0.8 0.47-0.56 0.76-1.29 0.76-2.08Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M5.2 11.03c0.02 0.3 0.28 0.54 0.58 0.52 0.3-0.02 0.54-0.28 0.52-0.58l-0.25-4.5c-0.02-0.3-0.28-0.54-0.58-0.52-0.3 0.02-0.54 0.28-0.52 0.58l0.25 4.5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8 5.95c0.3 0 0.55 0.25 0.55 0.55V11c0 0.3-0.25 0.55-0.55 0.55-0.3 0-0.55-0.25-0.55-0.55V6.5C7.45 6.2 7.7 5.95 8 5.95Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M11.05 6.53c0.02-0.3-0.22-0.56-0.52-0.58-0.3-0.02-0.56 0.22-0.58 0.52l-0.25 4.5c-0.02 0.3 0.22 0.56 0.52 0.58 0.3 0.02 0.56-0.22 0.58-0.52l0.25-4.5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8 0.35c1.55 0 2.8 1.05 3.09 2.5H14c0.36 0 0.65 0.3 0.65 0.65 0 0.36-0.3 0.65-0.65 0.65h-0.4l-0.45 6.7c-0.04 0.63-0.08 1.14-0.14 1.56-0.06 0.43-0.16 0.8-0.36 1.15-0.32 0.55-0.79 1-1.36 1.28-0.36 0.17-0.75 0.24-1.18 0.28-0.42 0.03-0.93 0.03-1.57 0.03H7.46c-0.64 0-1.15 0-1.57-0.03-0.43-0.04-0.82-0.11-1.18-0.28-0.57-0.28-1.04-0.73-1.36-1.28-0.2-0.35-0.3-0.72-0.36-1.15-0.06-0.42-0.1-0.93-0.14-1.57L2.39 4.15H2c-0.36 0-0.65-0.3-0.65-0.65 0-0.36 0.3-0.65 0.65-0.65h2.91C5.21 1.4 6.45 0.35 8 0.35Zm0 1.3c-0.83 0-1.5 0.48-1.74 1.2h3.48C9.5 2.13 8.83 1.65 8 1.65Zm-4.3 2.5l0.44 6.58c0.05 0.66 0.08 1.13 0.14 1.49 0.05 0.35 0.11 0.55 0.2 0.7 0.18 0.32 0.46 0.58 0.8 0.75 0.16 0.07 0.36 0.12 0.71 0.15 0.36 0.03 0.83 0.03 1.5 0.03H8.5c0.67 0 1.14 0 1.5-0.03 0.35-0.03 0.55-0.08 0.71-0.15 0.34-0.17 0.62-0.43 0.8-0.75 0.09-0.15 0.15-0.35 0.2-0.7 0.06-0.36 0.09-0.83 0.14-1.5l0.44-6.57H3.7Z"/>
|
||||
</vector>
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -46,31 +47,50 @@ object Scaffolds {
|
||||
Scaffold(
|
||||
snackbarHost = snackbarHost,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
titleContent(scrollBehavior.state.contentOffset, title)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onNavigationClick,
|
||||
Modifier.padding(end = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = navigationIconPainter,
|
||||
contentDescription = navigationContentDescription
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
scrolledContainerColor = SignalTheme.colors.colorSurface2
|
||||
)
|
||||
DefaultTopAppBar(
|
||||
title,
|
||||
titleContent,
|
||||
scrollBehavior,
|
||||
onNavigationClick,
|
||||
navigationIconPainter,
|
||||
navigationContentDescription
|
||||
)
|
||||
},
|
||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultTopAppBar(
|
||||
title: String,
|
||||
titleContent: @Composable (Float, String) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
onNavigationClick: () -> Unit,
|
||||
navigationIconPainter: Painter,
|
||||
navigationContentDescription: String?
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
titleContent(scrollBehavior.state.contentOffset, title)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onNavigationClick,
|
||||
Modifier.padding(end = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = navigationIconPainter,
|
||||
contentDescription = navigationContentDescription
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
scrolledContainerColor = SignalTheme.colors.colorSurface2
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
||||
@@ -72,6 +72,7 @@ dependencyResolutionManagement {
|
||||
library("androidx-multidex", "androidx.multidex:multidex:2.0.1")
|
||||
library("androidx-navigation-fragment-ktx", "androidx.navigation", "navigation-fragment-ktx").versionRef("androidx-navigation")
|
||||
library("androidx-navigation-ui-ktx", "androidx.navigation", "navigation-ui-ktx").versionRef("androidx-navigation")
|
||||
library("androidx-navigation-compose", "androidx.navigation", "navigation-compose").versionRef("androidx-navigation")
|
||||
library("androidx-lifecycle-viewmodel-ktx", "androidx.lifecycle", "lifecycle-viewmodel-ktx").versionRef("androidx-lifecycle")
|
||||
library("androidx-lifecycle-livedata-core", "androidx.lifecycle", "lifecycle-livedata").versionRef("androidx-lifecycle")
|
||||
library("androidx-lifecycle-livedata-ktx", "androidx.lifecycle", "lifecycle-livedata-ktx").versionRef("androidx-lifecycle")
|
||||
|
||||
@@ -1353,6 +1353,19 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="f49b02d47a1a1867f9a325ea5b884fdc7ee6b02d538d016b201d33e00d30c5e1" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.lifecycle" name="lifecycle-viewmodel-compose" version="2.6.2">
|
||||
<artifact name="lifecycle-viewmodel-compose-2.6.2.module">
|
||||
<sha256 value="55ba297e2fc68b1f9f673fbf9c2173e8deddf5aea23d9136f228eaa5fac57428" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.lifecycle" name="lifecycle-viewmodel-compose" version="2.7.0">
|
||||
<artifact name="lifecycle-viewmodel-compose-2.7.0.aar">
|
||||
<sha256 value="76403bb1599f6e2adc9b80372efb2674b8cbede2b0f1e6e85c4a94e10c7b94b0" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="lifecycle-viewmodel-compose-2.7.0.module">
|
||||
<sha256 value="76416d2af29301a4d571e72c679d8d5e89536962a0ab3ea2490b8b3bcab0d26f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.lifecycle" name="lifecycle-viewmodel-ktx" version="2.6.1">
|
||||
<artifact name="lifecycle-viewmodel-ktx-2.6.1.aar">
|
||||
<sha256 value="7fc0fa234a3321b1f34db5862b17da83d1c62c1bc66ec2fd4f1bb0a771acfabb" origin="Generated by Gradle"/>
|
||||
@@ -1509,6 +1522,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="1e02b0b2b949279f28a441f71aeecf54b11cb7632589ed3e253319d2a855c92e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigation" name="navigation-compose" version="2.7.6">
|
||||
<artifact name="navigation-compose-2.7.6.aar">
|
||||
<sha256 value="e4d9d5ab5387126c9704d6d6eae7afe21f5ec73b3efe9da0d6855f342d07c0de" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="navigation-compose-2.7.6.module">
|
||||
<sha256 value="b381d37bb4c2749562b3be71ad2405b2716451ac915b43d6480628d4f567cbb2" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigation" name="navigation-fragment" version="2.7.6">
|
||||
<artifact name="navigation-fragment-2.7.6.aar">
|
||||
<sha256 value="fa27d432027aee3559c4dd7b10f59622210885ea8e8fc285f7491beee2b214b2" origin="Generated by Gradle"/>
|
||||
|
||||
Reference in New Issue
Block a user