mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 15:03:16 +00:00
ui: badges WIP views (#7321)
This commit is contained in:
1 parent
4217c9ee84
commit
cd670548b9
31 files changed
+1750
-14
No files matched your search
+3
@@ -192,6 +192,7 @@ class AppPreferences {
|
||||
val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true)
|
||||
val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false)
|
||||
val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN, false)
|
||||
val supporterBannerShown = mkBoolPreference(SHARED_PREFS_SUPPORTER_BANNER_SHOWN, false)
|
||||
val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true)
|
||||
val showReportsInSupportChatAlert = mkBoolPreference(SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT, true)
|
||||
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
|
||||
@@ -273,6 +274,7 @@ class AppPreferences {
|
||||
hintPref(laNoticeShown, false),
|
||||
hintPref(oneHandUICardShown, false),
|
||||
hintPref(addressCreationCardShown, false),
|
||||
hintPref(supporterBannerShown, false),
|
||||
hintPref(liveMessageAlertShown, false),
|
||||
hintPref(signMessageAlertShown, false),
|
||||
hintPref(showHiddenProfilesNotice, true),
|
||||
@@ -464,6 +466,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice"
|
||||
private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown"
|
||||
private const val SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN = "AddressCreationCardShown"
|
||||
private const val SHARED_PREFS_SUPPORTER_BANNER_SHOWN = "SupporterBannerShown"
|
||||
private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert"
|
||||
private const val SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "ShowReportsInSupportChatAlert"
|
||||
private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase"
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.BadgeInfo
|
||||
import chat.simplex.common.model.BadgeStatus
|
||||
import chat.simplex.common.model.LocalBadge
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.views.helpers.NameWithBadge
|
||||
import chat.simplex.common.views.helpers.ProfileImage
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun BadgeUserPreview(level: BadgeLevel, modifier: Modifier = Modifier, trailing: @Composable () -> Unit = {}) {
|
||||
val user = chatModel.currentUser.value
|
||||
val displayName = user?.displayName ?: stringResource(MR.strings.badges_preview_my_nickname)
|
||||
val previewBadge = LocalBadge(
|
||||
badge = BadgeInfo(badgeType = level.badgeType),
|
||||
status = BadgeStatus.Active
|
||||
)
|
||||
Column(modifier, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
ProfileImage(size = 128.dp, image = user?.image)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
NameWithBadge(
|
||||
name = displayName,
|
||||
badge = previewBadge,
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
trailing()
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.res.MR
|
||||
|
||||
// TODO [badges]: replace lorem ipsum with the real copy once the badge protocol and privacy properties are documented.
|
||||
@Composable
|
||||
fun BadgesHowItWorksView() {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.padding(horizontal = 25.dp).padding(top = 8.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.badges_how_it_works_title),
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(stringResource(MR.strings.badges_how_it_works_p1), style = MaterialTheme.typography.body1)
|
||||
Text(stringResource(MR.strings.badges_how_it_works_p2), style = MaterialTheme.typography.body1)
|
||||
Text(stringResource(MR.strings.badges_how_it_works_p3), style = MaterialTheme.typography.body1)
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.OnboardingActionButton
|
||||
import chat.simplex.res.MR
|
||||
|
||||
// TODO [badges]: replace with types produced by the badge purchase API when it lands.
|
||||
enum class BadgePeriod {
|
||||
OneMonth,
|
||||
Subscribe;
|
||||
|
||||
val icon: dev.icerock.moko.resources.ImageResource
|
||||
get() = when (this) {
|
||||
OneMonth -> MR.images.ic_calendar
|
||||
Subscribe -> MR.images.ic_refresh
|
||||
}
|
||||
|
||||
val label: StringResource
|
||||
get() = when (this) {
|
||||
OneMonth -> MR.strings.badges_period_one_month
|
||||
Subscribe -> MR.strings.badges_period_subscribe
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BadgesPayView(level: BadgeLevel) {
|
||||
var selectedPeriod by remember { mutableStateOf(BadgePeriod.Subscribe) }
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
maxIntrinsicSize = true,
|
||||
) {
|
||||
Text(
|
||||
stringResource(level.title),
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
BadgeUserPreview(level = level, modifier = Modifier.padding(top = 4.dp))
|
||||
|
||||
Text(
|
||||
stringResource(level.tagline),
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp)
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f).heightIn(min = 20.dp))
|
||||
|
||||
// IntrinsicSize.Max + fillMaxHeight on children so both cards match the taller card's height
|
||||
// when 2-line labels at large fonts would otherwise size them differently.
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(IntrinsicSize.Max),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
PeriodCard(BadgePeriod.OneMonth, selectedPeriod, Modifier.weight(1f).fillMaxHeight()) { selectedPeriod = it }
|
||||
PeriodCard(BadgePeriod.Subscribe, selectedPeriod, Modifier.weight(1f).fillMaxHeight()) { selectedPeriod = it }
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f).heightIn(min = 20.dp))
|
||||
|
||||
// Replicates TextButtonBelowOnboardingButton spacing (7.5dp outer + 5dp inner) without a
|
||||
// TextButton so the footer has no hover/click affordance.
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
PayButton(level, selectedPeriod)
|
||||
Box(Modifier.padding(top = 7.5.dp, bottom = 7.5.dp).padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
Text(
|
||||
stringResource(billingFooter(selectedPeriod)).format(stubBillingDate()),
|
||||
Modifier.padding(vertical = 5.dp),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PeriodCard(period: BadgePeriod, selectedPeriod: BadgePeriod, modifier: Modifier, onSelect: (BadgePeriod) -> Unit) {
|
||||
val isSelected = period == selectedPeriod
|
||||
val borderColor = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f)
|
||||
// Light: transparent so card matches page background. Dark: subtle gray tint for visible contrast.
|
||||
val cardBackground = if (isInDarkTheme()) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||
else MaterialTheme.colors.background
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
Column(
|
||||
modifier
|
||||
.clip(shape)
|
||||
.background(cardBackground, shape)
|
||||
.border(2.dp, borderColor, shape)
|
||||
.clickable { onSelect(period) }
|
||||
.padding(vertical = 30.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(period.icon),
|
||||
contentDescription = null,
|
||||
tint = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Text(stringResource(period.label), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PayButton(level: BadgeLevel, selectedPeriod: BadgePeriod) {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = if (selectedPeriod == BadgePeriod.Subscribe) MR.strings.badges_pay_monthly else MR.strings.badges_pay_once,
|
||||
labelArg = level.priceAmount,
|
||||
onboarding = null,
|
||||
onclick = {
|
||||
// TODO [badges] wire to purchase API when it lands.
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun billingFooter(period: BadgePeriod): StringResource = when (period) {
|
||||
BadgePeriod.Subscribe -> MR.strings.badges_billing_footer_subscribe
|
||||
BadgePeriod.OneMonth -> MR.strings.badges_billing_footer_one_month
|
||||
}
|
||||
|
||||
// TODO [badges] source the actual date from the purchase state machine when wired.
|
||||
private fun stubBillingDate(): String {
|
||||
val date = java.time.LocalDate.of(2026, 7, 22)
|
||||
val formatter = java.time.format.DateTimeFormatter.ofLocalizedDate(java.time.format.FormatStyle.LONG)
|
||||
return date.format(formatter)
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.res.MR
|
||||
|
||||
// TODO [badges]: implement input field, server verification and success/failure states when the redeem API is defined.
|
||||
@Composable
|
||||
fun BadgesRedeemCodeView() {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.padding(horizontal = 25.dp).padding(top = 8.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.badges_redeem_code_button),
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.darkStops
|
||||
import chat.simplex.common.views.newchat.gradientPoints
|
||||
import chat.simplex.common.views.newchat.lightStops
|
||||
import chat.simplex.common.views.onboarding.HowItWorks
|
||||
import chat.simplex.common.views.onboarding.OnboardingActionButton
|
||||
import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun BadgesSupportSimplexView() {
|
||||
// TODO [badges] gate on user badge status (no badge → this view, active → "Manage your badge")
|
||||
ColumnWithScrollBar(
|
||||
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
maxIntrinsicSize = true,
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.badges_support_simplex_title),
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(MR.strings.badges_support_simplex_body),
|
||||
style = MaterialTheme.typography.body1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
val primary = MaterialTheme.colors.primary
|
||||
TextButton({
|
||||
ModalManager.start.showModal { HowItWorks(user = chatModel.currentUser.value, onboardingStage = null, titleColor = primary) }
|
||||
}) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.primary)
|
||||
Text(stringResource(MR.strings.badges_why_simplex_is_built), color = MaterialTheme.colors.primary, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
PhoneSupporterHero(Modifier.fillMaxWidth(0.55f))
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
ChooseLevelButton()
|
||||
TextButtonBelowOnboardingButton(
|
||||
text = stringResource(MR.strings.badges_redeem_code_button),
|
||||
onClick = { ModalManager.start.showModal { BadgesRedeemCodeView() } }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChooseLevelButton() {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = MR.strings.badges_choose_your_level,
|
||||
onboarding = null,
|
||||
onclick = {
|
||||
ModalManager.start.showModal { BadgesYourLevelView() }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun PhoneSupporterHero(modifier: Modifier = Modifier) {
|
||||
val isDark = isInDarkTheme()
|
||||
if (BuildConfigCommon.SIMPLEX_ASSETS) {
|
||||
Image(
|
||||
painterResource(if (isDark) MR.images.phone_supporter_light else MR.images.phone_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
var size by remember { mutableStateOf(IntSize.Zero) }
|
||||
val stops = if (isDark) darkStops else lightStops
|
||||
val scale = if (isDark) 1.5f else 1.2f
|
||||
val brush = remember(size, isDark) {
|
||||
if (size.width > 0 && size.height > 0) {
|
||||
val aspect = size.height.toFloat() / size.width.toFloat()
|
||||
val gp = gradientPoints(aspect, scale)
|
||||
Brush.linearGradient(
|
||||
colorStops = stops,
|
||||
start = Offset(gp.startX * size.width, gp.startY * size.height),
|
||||
end = Offset(gp.endX * size.width, gp.endY * size.height)
|
||||
)
|
||||
} else {
|
||||
Brush.linearGradient(colorStops = stops)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(brush)
|
||||
.onSizeChanged { size = it },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painterResource(MR.images.badge_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(96.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.BadgeType
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.OnboardingActionButton
|
||||
import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton
|
||||
import chat.simplex.res.MR
|
||||
|
||||
// TODO [badges]: replace with types produced by the badge purchase API when it lands.
|
||||
enum class BadgeLevel {
|
||||
Supporter,
|
||||
Legend;
|
||||
|
||||
val title: StringResource
|
||||
get() = when (this) {
|
||||
Supporter -> MR.strings.badges_level_supporter
|
||||
Legend -> MR.strings.badges_level_legend
|
||||
}
|
||||
|
||||
val filesDescription: StringResource
|
||||
get() = when (this) {
|
||||
Supporter -> MR.strings.badges_level_supporter_files
|
||||
Legend -> MR.strings.badges_level_legend_files
|
||||
}
|
||||
|
||||
val priceAmount: String
|
||||
get() = when (this) {
|
||||
Supporter -> "$7"
|
||||
Legend -> "$70"
|
||||
}
|
||||
|
||||
val tagline: StringResource
|
||||
get() = when (this) {
|
||||
Supporter -> MR.strings.badges_level_supporter_tagline
|
||||
Legend -> MR.strings.badges_level_legend_tagline
|
||||
}
|
||||
|
||||
val badgeType: BadgeType
|
||||
get() = when (this) {
|
||||
Supporter -> BadgeType.Supporter
|
||||
Legend -> BadgeType.Legend
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BadgesYourLevelView() {
|
||||
var selectedLevel by remember { mutableStateOf(BadgeLevel.Supporter) }
|
||||
|
||||
ColumnWithScrollBar(
|
||||
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
maxIntrinsicSize = true,
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.badges_your_level_title),
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
BadgeUserPreview(level = selectedLevel, modifier = Modifier.padding(top = 4.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_keyboard_arrow_down),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f).heightIn(min = 20.dp))
|
||||
|
||||
// IntrinsicSize.Max + fillMaxHeight on children so both cards match the taller card's height
|
||||
// when 2-line labels at large fonts would otherwise size them differently.
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(IntrinsicSize.Max),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
LevelCard(BadgeLevel.Supporter, selectedLevel, Modifier.weight(1f).fillMaxHeight()) { selectedLevel = it }
|
||||
LevelCard(BadgeLevel.Legend, selectedLevel, Modifier.weight(1f).fillMaxHeight()) { selectedLevel = it }
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f).heightIn(min = 20.dp))
|
||||
|
||||
// Nested Column with no spacing so the TextButtonBelowOnboardingButton sits directly under
|
||||
// the action button (matches onboarding pattern where its own 7.5dp top padding is the gap).
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
ContinueButton(selectedLevel)
|
||||
TextButtonBelowOnboardingButton(
|
||||
text = stringResource(MR.strings.badges_how_it_works_button),
|
||||
icon = painterResource(MR.images.ic_info),
|
||||
onClick = { ModalManager.start.showModal { BadgesHowItWorksView() } }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LevelCard(level: BadgeLevel, selectedLevel: BadgeLevel, modifier: Modifier, onSelect: (BadgeLevel) -> Unit) {
|
||||
val isSelected = level == selectedLevel
|
||||
val borderColor = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f)
|
||||
// Light: transparent so card matches page background. Dark: subtle gray tint for visible contrast.
|
||||
val cardBackground = if (isInDarkTheme()) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||
else MaterialTheme.colors.background
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
Column(
|
||||
modifier
|
||||
.clip(shape)
|
||||
.background(cardBackground, shape)
|
||||
.border(2.dp, borderColor, shape)
|
||||
.clickable { onSelect(level) }
|
||||
.padding(vertical = 20.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Image(
|
||||
painterResource(badgeImage(level.badgeType)),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(60.dp)
|
||||
)
|
||||
Text(stringResource(level.title), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center)
|
||||
Text(stringResource(level.filesDescription), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
||||
Text(stringResource(MR.strings.badges_price_monthly).format(level.priceAmount), style = MaterialTheme.typography.body1, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContinueButton(selectedLevel: BadgeLevel) {
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = MR.strings.badges_continue,
|
||||
onboarding = null,
|
||||
onclick = {
|
||||
ModalManager.start.showModal { BadgesPayView(selectedLevel) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.darkStops
|
||||
import chat.simplex.common.views.newchat.gradientPoints
|
||||
import chat.simplex.common.views.newchat.lightStops
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun SupportSimpleXBanner(onTap: () -> Unit, onDismiss: () -> Unit) {
|
||||
val cardCornerRadius = 16.dp
|
||||
// grows linearly with system font but never shrinks below the default so small-font users see the
|
||||
// same baseline; the card Row uses heightIn(min = cardHeight) and grows further when 2-line text
|
||||
// wraps at very large fonts. Hero stays fixed so its above-card overhang shrinks at very large fonts.
|
||||
val cardHeight = (72.dp * fontSizeMultiplier).coerceAtLeast(72.dp)
|
||||
// matches OneHandUICard's segment icon leading so the text aligns with it in the list
|
||||
val cardLeadingPadding = 16.dp
|
||||
val cardTrailingPadding = 8.dp
|
||||
val heroWidth = 110.dp
|
||||
// shorter than the natural drawn height so ContentScale.Crop slices the phone body at card bottom
|
||||
val heroVisibleHeight = 108.dp
|
||||
// hero right edge sits at the dismiss X's icon left edge (X: outer 4pt + inner-pad 8 + half of 16pt icon)
|
||||
val heroTrailingPadding = 28.dp
|
||||
val textToHeroGap = 6.dp
|
||||
|
||||
val isDark = isInDarkTheme()
|
||||
var cardSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
val brush = remember(isDark, cardSize) { gradientBrush(isDark, cardSize) }
|
||||
|
||||
// Layout sizes to the card; hero is placed at y = cardHeight - heroHeight (negative → hero
|
||||
// overhangs above card at normal fonts, 0/positive → hero fits inside card at large fonts).
|
||||
Layout(content = {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cardHeight)
|
||||
.clip(RoundedCornerShape(cardCornerRadius))
|
||||
.background(brush)
|
||||
.clickable(onClick = onTap)
|
||||
.onSizeChanged { cardSize = it }
|
||||
.padding(
|
||||
start = cardLeadingPadding,
|
||||
end = cardTrailingPadding + heroWidth + heroTrailingPadding + textToHeroGap,
|
||||
top = 12.dp,
|
||||
bottom = 12.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
stringResource(MR.strings.badges_banner_title),
|
||||
style = MaterialTheme.typography.body1,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
stringResource(MR.strings.badges_banner_subtitle),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Same X pattern as OneHandUICard: circle-clipped clickable region with inner padding for hit area.
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
contentDescription = stringResource(MR.strings.icon_descr_close_button),
|
||||
tint = if (isDark) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(end = 4.dp, top = 4.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.badges_banner_title),
|
||||
text = generalGetString(MR.strings.badges_banner_dismiss_message),
|
||||
onConfirm = onDismiss
|
||||
)
|
||||
}
|
||||
.padding(8.dp)
|
||||
.size(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
HeroThumbnail(
|
||||
heroWidth = heroWidth,
|
||||
heroVisibleHeight = heroVisibleHeight,
|
||||
cardHeight = cardHeight,
|
||||
trailingPadding = heroTrailingPadding
|
||||
)
|
||||
}) { measurables, constraints ->
|
||||
val cardPlaceable = measurables[0].measure(constraints)
|
||||
val heroPlaceable = measurables[1].measure(constraints.copy(minWidth = 0, minHeight = 0))
|
||||
layout(cardPlaceable.width, cardPlaceable.height) {
|
||||
cardPlaceable.place(0, 0)
|
||||
heroPlaceable.place(cardPlaceable.width - heroPlaceable.width, cardPlaceable.height - heroPlaceable.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeroThumbnail(heroWidth: Dp, heroVisibleHeight: Dp, cardHeight: Dp, trailingPadding: Dp) {
|
||||
if (BuildConfigCommon.SIMPLEX_ASSETS) {
|
||||
// draws at natural aspect, top-aligned in a shorter slot; ContentScale.Crop cuts the overflow at card bottom
|
||||
Image(
|
||||
painterResource(if (isInDarkTheme()) MR.images.phone_supporter_light else MR.images.phone_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
alignment = Alignment.TopCenter,
|
||||
modifier = Modifier.padding(end = trailingPadding).size(width = heroWidth, height = heroVisibleHeight)
|
||||
)
|
||||
} else {
|
||||
val badgeSize = 48.dp
|
||||
Image(
|
||||
painterResource(MR.images.badge_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.padding(end = trailingPadding + 12.dp, top = (cardHeight - badgeSize) / 2, bottom = (cardHeight - badgeSize) / 2)
|
||||
.size(badgeSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Geometry-aware gradient with asymmetric scale: start (dark) pushed further below the card than
|
||||
// end (warm) is above, so card-middle lands at the bright/mid-transition stop, not the dark region.
|
||||
private fun gradientBrush(isDark: Boolean, size: IntSize): Brush {
|
||||
val stops = if (isDark) darkStops else lightStops
|
||||
if (size.width == 0 || size.height == 0) return Brush.linearGradient(colorStops = stops)
|
||||
val w = size.width.toFloat()
|
||||
val h = size.height.toFloat()
|
||||
val startScale = if (isDark) 3.0f else 2.5f
|
||||
val endScale = if (isDark) 2.1f else 1.7f
|
||||
val gp = gradientPoints(h / w, 1.0f)
|
||||
val sx = 0.5f + (gp.startX - 0.5f) * startScale
|
||||
val sy = 0.5f + (gp.startY - 0.5f) * startScale
|
||||
val ex = 0.5f + (gp.endX - 0.5f) * endScale
|
||||
val ey = 0.5f + (gp.endY - 0.5f) * endScale
|
||||
return Brush.linearGradient(
|
||||
colorStops = stops,
|
||||
start = Offset(sx * w, sy * h),
|
||||
end = Offset(ex * w, ey * h)
|
||||
)
|
||||
}
|
||||
+21
-7
@@ -41,6 +41,7 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.Call
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
import chat.simplex.common.views.chat.topPaddingToContent
|
||||
import chat.simplex.common.views.badges.*
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.common.views.onboarding.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -912,6 +913,7 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state }
|
||||
val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state }
|
||||
val supporterBannerShown = remember { appPrefs.supporterBannerShown.state }
|
||||
val activeFilter = remember { chatModel.activeChatTagFilter }
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
@@ -1000,6 +1002,16 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
ToggleChatListCard()
|
||||
}
|
||||
}
|
||||
if (!supporterBannerShown.value && chatModel.chats.value.size > 3) {
|
||||
item {
|
||||
Box(Modifier.zIndex(1f).padding(16.dp)) {
|
||||
SupportSimpleXBanner(
|
||||
onTap = { ModalManager.start.showModal { BadgesSupportSimplexView() } },
|
||||
onDismiss = { appPrefs.supporterBannerShown.set(true) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
|
||||
chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
@@ -1025,13 +1037,15 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
} else {
|
||||
NavigationBarBackground(oneHandUI.value, true)
|
||||
}
|
||||
if (!oneHandUICardShown.value) {
|
||||
LaunchedEffect(chats.size) {
|
||||
if (chats.size >= 3) {
|
||||
appPrefs.oneHandUICardShown.set(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
// TEMP-DISABLED-FOR-BADGES-QA: auto-hide of ToggleChatListCard at 3+ chats blocks visual QA of
|
||||
// the SupportSimpleXBanner alongside it. Restore before merging.
|
||||
// if (!oneHandUICardShown.value) {
|
||||
// LaunchedEffect(chats.size) {
|
||||
// if (chats.size >= 3) {
|
||||
// appPrefs.oneHandUICardShown.set(true)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
LaunchedEffect(activeFilter.value) {
|
||||
searchText.value = TextFieldValue("")
|
||||
|
||||
+1
-1
@@ -245,7 +245,7 @@ fun showBadgeInfoAlert(name: String, badge: LocalBadge, uriHandler: UriHandler)
|
||||
}
|
||||
}
|
||||
|
||||
private fun badgeImage(t: BadgeType): ImageResource = when (t) {
|
||||
fun badgeImage(t: BadgeType): ImageResource = when (t) {
|
||||
is BadgeType.Legend -> MR.images.badge_legend
|
||||
is BadgeType.Investor -> MR.images.badge_investor
|
||||
else -> MR.images.badge_supporter // Supporter + Unknown
|
||||
|
||||
+8
-2
@@ -7,6 +7,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
@@ -22,12 +23,12 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
|
||||
@Composable
|
||||
fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null) {
|
||||
fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null, titleColor: Color = Color.Unspecified) {
|
||||
Column(Modifier.fillMaxSize().padding(horizontal = if (appPlatform.isDesktop) DEFAULT_PADDING * 2 else DEFAULT_PADDING)) {
|
||||
Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier))
|
||||
val paraPadding = PaddingValues(bottom = if (appPlatform.isDesktop) 10.dp else 12.dp)
|
||||
Column(Modifier.weight(1f).padding(bottom = DEFAULT_PADDING).verticalScroll(rememberScrollState())) {
|
||||
Text(stringResource(MR.strings.why_built_heading), style = MaterialTheme.typography.h1, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
Text(stringResource(MR.strings.why_built_heading), style = MaterialTheme.typography.h1, color = titleColor, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
ReadableText(MR.strings.why_built_p1, padding = paraPadding)
|
||||
ReadableText(MR.strings.why_built_p2, padding = paraPadding)
|
||||
ReadableText(MR.strings.why_built_p3, padding = paraPadding)
|
||||
@@ -45,6 +46,11 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>?
|
||||
OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.fullscreen.closeModal() })
|
||||
TextButtonBelowOnboardingButton("", null)
|
||||
}
|
||||
} else {
|
||||
// No button below — add breathing room at the bottom on both platforms.
|
||||
// Android also gets nav-bar inset so content doesn't run under the gesture bar.
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
if (appPlatform.isAndroid) Spacer(Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -190,12 +190,14 @@ expect fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference
|
||||
fun OnboardingActionButton(
|
||||
modifier: Modifier = Modifier,
|
||||
labelId: StringResource,
|
||||
labelArg: String? = null,
|
||||
onboarding: OnboardingStage?,
|
||||
enabled: Boolean = true,
|
||||
icon: Painter? = null,
|
||||
iconColor: Color = Color.White,
|
||||
onclick: (() -> Unit)?
|
||||
) {
|
||||
val label = if (labelArg != null) stringResource(labelId).format(labelArg) else stringResource(labelId)
|
||||
Button(
|
||||
onClick = {
|
||||
onclick?.invoke()
|
||||
@@ -211,9 +213,9 @@ fun OnboardingActionButton(
|
||||
colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary, disabledBackgroundColor = MaterialTheme.colors.secondary)
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(icon, stringResource(labelId), Modifier.padding(end = DEFAULT_PADDING_HALF), tint = iconColor)
|
||||
Icon(icon, label, Modifier.padding(end = DEFAULT_PADDING_HALF), tint = iconColor)
|
||||
}
|
||||
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium)
|
||||
Text(label, style = MaterialTheme.typography.h2, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+47
@@ -8,6 +8,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -940,8 +941,54 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
),
|
||||
)
|
||||
),
|
||||
// TODO [badges] finalise copy + Read more link before v7.1 ships.
|
||||
VersionDescription(
|
||||
version = "v7.1",
|
||||
post = null,
|
||||
features = listOf(
|
||||
VersionFeature.FeatureView(
|
||||
icon = null,
|
||||
titleId = MR.strings.v7_1_supporter_badge_title,
|
||||
view = { SupporterBadgeWhatsNew() }
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun SupporterBadgeWhatsNew() {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.Top) {
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(
|
||||
stringResource(MR.strings.v7_1_supporter_badge_title),
|
||||
style = MaterialTheme.typography.h3,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
stringResource(MR.strings.v7_1_supporter_badge_body),
|
||||
style = MaterialTheme.typography.body1,
|
||||
maxLines = 10
|
||||
)
|
||||
}
|
||||
val isDark = isInDarkTheme()
|
||||
if (BuildConfigCommon.SIMPLEX_ASSETS) {
|
||||
Image(
|
||||
painterResource(if (isDark) MR.images.phone_supporter_light else MR.images.phone_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(110.dp)
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painterResource(MR.images.badge_supporter),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(72.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val lastVersion = versionDescriptions.last().version
|
||||
|
||||
fun setLastVersionDefault(m: ChatModel) {
|
||||
|
||||
+12
@@ -26,6 +26,7 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.badges.BadgesSupportSimplexView
|
||||
import chat.simplex.common.views.database.DatabaseView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.migration.MigrateFromDeviceView
|
||||
@@ -101,6 +102,17 @@ fun SettingsLayout(
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView {
|
||||
// Direct showModal (no settings / cardScreen flags) — settings-style card chrome would render
|
||||
// a gray top bar / back button that badges views don't want (they have their own inline titles).
|
||||
SectionItemView(click = { ModalManager.start.showModal { BadgesSupportSimplexView() } }) {
|
||||
Image(painterResource(MR.images.badge_supporter), stringResource(MR.strings.supporter_perks), Modifier.size(24.dp))
|
||||
TextIconSpaced()
|
||||
Text(stringResource(MR.strings.supporter_perks))
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.advanced_settings)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped)
|
||||
if (appPlatform == AppPlatform.ANDROID) {
|
||||
|
||||
@@ -3201,4 +3201,36 @@
|
||||
<string name="badge_unverified_desc">This badge could not be verified and may not be genuine.</string>
|
||||
<string name="badge_unknown_key_title">Badge cannot be verified</string>
|
||||
<string name="badge_unknown_key_desc">The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge.</string>
|
||||
<string name="badges_level_supporter">Supporter</string>
|
||||
<string name="badges_level_legend">Legend</string>
|
||||
<string name="badges_level_supporter_files">Send 2GB files</string>
|
||||
<string name="badges_level_legend_files">Send 5GB files</string>
|
||||
<string name="badges_price_monthly">%1$s/month</string>
|
||||
<string name="badges_pay_monthly">Pay %1$s/month</string>
|
||||
<string name="badges_pay_once">Pay %1$s</string>
|
||||
<string name="badges_level_supporter_tagline">Optional profile badge\nand 2GB files</string>
|
||||
<string name="badges_level_legend_tagline">Optional profile badge\nand 5GB files</string>
|
||||
<string name="badges_period_one_month">1 month</string>
|
||||
<string name="badges_period_subscribe">Subscribe</string>
|
||||
<string name="badges_billing_footer_subscribe">Renews on %1$s. Cancel anytime.</string>
|
||||
<string name="badges_billing_footer_one_month">Ends on %1$s.</string>
|
||||
<string name="badges_your_level_title">Your level</string>
|
||||
<string name="badges_support_simplex_title">Support SimpleX</string>
|
||||
<string name="badges_support_simplex_body">SimpleX doesn\'t sell ads or data. It\'s funded by its users and by investors who share the mission. You can support the project and show a badge on your profile.</string>
|
||||
<string name="badges_why_simplex_is_built">Why SimpleX is built.</string>
|
||||
<string name="badges_choose_your_level">Choose your level</string>
|
||||
<string name="badges_redeem_code_button">Redeem badge code</string>
|
||||
<string name="badges_continue">Continue</string>
|
||||
<string name="badges_how_it_works_button">How private badges work</string>
|
||||
<string name="badges_how_it_works_title">How private badges work</string>
|
||||
<string name="badges_how_it_works_p1">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</string>
|
||||
<string name="badges_how_it_works_p2">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</string>
|
||||
<string name="badges_how_it_works_p3">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</string>
|
||||
<string name="badges_preview_my_nickname">My nickname</string>
|
||||
<string name="badges_banner_title">Support SimpleX</string>
|
||||
<string name="badges_banner_subtitle">Get badge + files up to 5GB</string>
|
||||
<string name="badges_banner_dismiss_message">You can support SimpleX later in Settings.</string>
|
||||
<string name="supporter_perks">Supporter perks</string>
|
||||
<string name="v7_1_supporter_badge_title">Supporter badge ❤️</string>
|
||||
<string name="v7_1_supporter_badge_body">Help keep the network running — send files up to 2 GB.</string>
|
||||
</resources>
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
Reference in new issue
Block a user