Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1235,21 +1235,19 @@
<string name="title_minimumBalanceRequired">Minimum Balance Required</string>
<!-- And after, when the row below it already says which token (node 10127:118194). -->
<string name="subtitle_groupBalanceRequirement">People won\'t be able to join this chat if their balance is less than this amount</string>
<!-- %1$s is the community currency's name. -->
<string name="subtitle_groupBalanceRequirementToken">People need this amount of %1$s to join this chat.</string>
<string name="subtitle_groupBalanceRequirementAny">People need this amount of any community currency to join this chat.</string>
<string name="action_create">Create</string>
<!-- StartChat rejects a creator who does not meet their own bar, so the form says so before the
call rather than surfacing RULES_NOT_SATISFIED as a failure. -->
<string name="error_title_groupRuleNotSelfSatisfied">You Need More %1$s</string>
<string name="error_description_groupRuleNotSelfSatisfied">You have to meet the balance requirement you set to create this group</string>
<!-- The same refusal for a Total Balance rule, which names no one token to need more of. -->
<!-- The same refusal for an any-currency rule, which names no one token to need more of. -->
<string name="error_title_groupRuleNotSelfSatisfiedTotal">You Need a Higher Balance</string>
<!-- The currency sheet's header card (node 10370:997): a rule measured against every holding added up. -->
<string name="title_totalBalance">Total Balance</string>
<!-- %1$s is the total of every balance, %2$s the requirement it is measured against. -->
<string name="subtitle_totalBalanceMeets">You have %1$s total · meets %2$s</string>
<!-- The short variant, shown in the error colour (node 10372:1126). -->
<string name="subtitle_totalBalanceNeeds">You have %1$s total · need %2$s</string>
<!-- Before an amount is set there is nothing to meet. -->
<string name="subtitle_totalBalance">You have %1$s total</string>
<!-- Section label over the form's currency row, and the currency sheet's last entry (node 11745:1054). -->
<string name="title_communityCurrency">Community Currency</string>
<string name="title_anyCommunityCurrency">Any Community Currency</string>
<string name="title_specificCurrency">Specific Currency</string>
<string name="error_title_groupTitleNotAllowed">This Group Name is Not Allowed</string>
<string name="error_title_groupRulesInvalid">These Rules Aren\'t Allowed</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import com.getcode.opencode.model.financial.Fiat
import com.getcode.opencode.model.financial.Rate
import com.getcode.opencode.model.financial.Token
import com.getcode.opencode.model.financial.TokenWithBalance
import com.getcode.opencode.model.financial.sum
import com.getcode.util.resources.ContentReader
import com.getcode.util.resources.ResourceHelper
import com.getcode.view.BaseViewModel
Expand Down Expand Up @@ -152,13 +151,6 @@ internal class CreateGroupViewModel @AssistedInject constructor(
val currencyName: String?
get() = token?.name

/**
* Every holding added up, in USD — what an any-currency rule is measured against, and what
* the currency sheet's header card reports.
*/
val totalBalance: Fiat
get() = balances.map { it.balance }.sum()

/**
* The rules the draft describes, once it describes any.
*
Expand All @@ -177,13 +169,6 @@ internal class CreateGroupViewModel @AssistedInject constructor(
val access: GroupAccess?
get() = rules?.let(::accessFor)

/**
* Whether the total meets the amount — the header card's `meets` or `need`, which it shows
* whichever currency is selected. Null until there is an amount to measure against.
*/
val allCurrenciesSatisfied: Boolean?
get() = amount?.let { accessFor(rulesFor(GroupCurrency.All, it)) is GroupAccess.Eligible }

private fun accessFor(rules: ChatRules): GroupAccess = GroupAccess.evaluate(
isMember = false,
rules = rules,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.flipcash.app.tipping.internal
import com.getcode.solana.keys.Mint

/**
* What a group's balance requirement is measured in — the currency sheet's header card (node
* 10370:997) or one of the tokens under it (node 10372:1015).
* What a group's balance requirement is measured in — the currency sheet's "Any Community
* Currency" entry or one of the tokens above it.
*
* [All] is not the absence of a choice. `MinimumBalanceRequirement.mints` left empty means "any
* currency", and the server measures it against every holding added up in USD, USDF included; so it
Expand All @@ -23,3 +23,11 @@ internal sealed interface GroupCurrency {
override val mints: List<Mint> = listOf(mint)
}
}

/**
* The rows the currency sheet offers as specific currencies: everything but USDF. Dollars aren't a
* community currency, and "Any Community Currency" already covers them in the USD total. The sheet
* is the only thing that sets [GroupCurrency.Specific], so a draft can't hold USDF.
*/
internal fun <T> List<T>.withoutUsdf(mintOf: (T) -> Mint): List<T> =
filterNot { mintOf(it) == Mint.usdf }
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
Expand All @@ -35,6 +36,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
Expand Down Expand Up @@ -311,7 +313,7 @@ private fun IdentityCard(
}

/**
* The balance rule: which currency, and how much of it.
* The balance rule: which currency (its own section, over the amount), and how much of it.
*/
@Composable
private fun RequirementSection(
Expand All @@ -326,6 +328,20 @@ private fun RequirementSection(
) {
Text(
modifier = Modifier.padding(horizontal = CodeTheme.dimens.grid.x3),
text = stringResource(R.string.title_communityCurrency),
style = LabelStyle,
color = White50,
)

MintRow(state = state, onClick = onChangeCurrency)

Text(
// Its own section: the extra top inset is the gap between sections, not a label's.
modifier = Modifier.padding(
start = CodeTheme.dimens.grid.x3,
end = CodeTheme.dimens.grid.x3,
top = CodeTheme.dimens.grid.x3,
),
text = stringResource(R.string.title_minimumBalanceRequired),
style = LabelStyle,
color = White50,
Expand All @@ -339,8 +355,6 @@ private fun RequirementSection(
.padding(CodeTheme.dimens.grid.x3),
verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.staticGrid.x2),
) {
MintRow(state = state, onClick = onChangeCurrency)

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
Expand Down Expand Up @@ -384,29 +398,40 @@ private fun RequirementSection(

Text(
modifier = Modifier.padding(horizontal = CodeTheme.dimens.grid.x3),
text = stringResource(
if (unsatisfied) {
R.string.error_description_groupRuleNotSelfSatisfied
} else {
R.string.subtitle_groupBalanceRequirement
}
),
text = when {
unsatisfied -> stringResource(R.string.error_description_groupRuleNotSelfSatisfied)
state.currency == GroupCurrency.All ->
stringResource(R.string.subtitle_groupBalanceRequirementAny)
// A picked token that has left the list has no name; the rule still holds for it.
else -> state.currencyName?.let {
stringResource(R.string.subtitle_groupBalanceRequirementToken, it)
} ?: stringResource(R.string.subtitle_groupBalanceRequirement)
},
style = CodeTheme.typography.caption.copy(fontSize = 13.sp, lineHeight = 17.sp),
color = if (unsatisfied) CodeTheme.colors.errorText else White50,
)
}
}

/**
* The currency row, dressed as the group info screen's Token row: 56 high, 16 in from the edges,
* a 32 icon and the name, and a chevron in textSecondary on the 12-radius card background.
*/
@Composable
private fun MintRow(state: CreateGroupViewModel.State, onClick: () -> Unit) {
Row(
modifier = Modifier.clickable(onClick = onClick),
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1),
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(MintRowRadius))
.background(White05)
.height(MintRowHeight)
.clickable(onClick = onClick)
.padding(horizontal = MintRowInset),
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.staticGrid.x2),
verticalAlignment = Alignment.CenterVertically,
) {
val allCurrencies = state.currency == GroupCurrency.All
if (allCurrencies) {
// Node 10364:1059 — the sheet's card glyph, scaled to the row.
val anyCurrency = state.currency == GroupCurrency.All
if (anyCurrency) {
AllCurrenciesIcon(discSize = MintIconSize, iconSize = AllCurrenciesGlyphSize)
} else {
AsyncImage(
Expand All @@ -424,25 +449,24 @@ private fun MintRow(state: CreateGroupViewModel.State, onClick: () -> Unit) {

// A picked token whose balance has since left the list has no name to show, so the row
// falls back to the sheet's own title rather than rendering a blank, tappable line.
val name = if (allCurrencies) {
stringResource(R.string.title_totalBalance)
val name = if (anyCurrency) {
stringResource(R.string.title_anyCommunityCurrency)
} else {
state.currencyName
}
Text(
modifier = Modifier.weight(1f),
text = name ?: stringResource(R.string.title_selectCurrency),
style = CodeTheme.typography.textMedium.copy(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
),
color = if (name == null) White50 else CodeTheme.colors.textMain,
style = CodeTheme.typography.textMedium,
color = if (name == null) CodeTheme.colors.textSecondary else CodeTheme.colors.textMain,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)

Icon(
painter = painterResource(R.drawable.ic_chevron_down_small),
painter = painterResource(R.drawable.ic_chevron_right),
contentDescription = null,
tint = CodeTheme.colors.textMain,
modifier = Modifier.size(CodeTheme.dimens.staticGrid.x5),
tint = CodeTheme.colors.textSecondary,
)
}
}
Expand All @@ -469,8 +493,11 @@ private fun AmountChip(

private val AvatarSize = 74.dp
private val CameraIconSize = 40.dp
private val MintIconSize = 20.dp
private val AllCurrenciesGlyphSize = 12.dp
private val MintIconSize = 32.dp
private val AllCurrenciesGlyphSize = 20.dp
private val MintRowHeight = 56.dp
private val MintRowInset = 16.dp
private val MintRowRadius = 12.dp
private val ChipHeight = 60.dp

private val LabelStyle
Expand Down
Loading
Loading