previous PR fixes

This commit is contained in:
nrobi144
2026-01-07 07:12:00 +02:00
parent fd83560852
commit a9141763e1
9 changed files with 153 additions and 28 deletions
+10
View File
@@ -5,6 +5,7 @@ plugins {
alias(libs.plugins.androidLibrary) alias(libs.plugins.androidLibrary)
alias(libs.plugins.jetbrainsComposeCompiler) alias(libs.plugins.jetbrainsComposeCompiler)
alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.mokoResources)
} }
android { android {
@@ -75,6 +76,10 @@ kotlin {
// Immutable collections // Immutable collections
api(libs.kotlinx.collections.immutable) api(libs.kotlinx.collections.immutable)
// Moko Resources for KMP string resources
api(libs.moko.resources)
api(libs.moko.resources.compose)
} }
} }
@@ -124,3 +129,8 @@ kotlin {
} }
} }
} }
multiplatformResources {
resourcesPackage.set("com.vitorpamplona.amethyst.commons")
resourcesClassName.set("SharedRes")
}
@@ -34,6 +34,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes
import dev.icerock.moko.resources.compose.stringResource
/** /**
* A centered loading state with a progress indicator and message. * A centered loading state with a progress indicator and message.
@@ -67,8 +69,10 @@ fun EmptyState(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
description: String? = null, description: String? = null,
onRefresh: (() -> Unit)? = null, onRefresh: (() -> Unit)? = null,
refreshLabel: String = "Refresh", refreshLabel: String? = null,
) { ) {
val actualRefreshLabel = refreshLabel ?: stringResource(SharedRes.strings.action_refresh)
Column( Column(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -90,7 +94,7 @@ fun EmptyState(
if (onRefresh != null) { if (onRefresh != null) {
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
OutlinedButton(onClick = onRefresh) { OutlinedButton(onClick = onRefresh) {
Text(refreshLabel) Text(actualRefreshLabel)
} }
} }
} }
@@ -104,8 +108,10 @@ fun ErrorState(
message: String, message: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null, onRetry: (() -> Unit)? = null,
retryLabel: String = "Try Again", retryLabel: String? = null,
) { ) {
val actualRetryLabel = retryLabel ?: stringResource(SharedRes.strings.action_try_again)
Column( Column(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -119,7 +125,7 @@ fun ErrorState(
if (onRetry != null) { if (onRetry != null) {
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Button(onClick = onRetry) { Button(onClick = onRetry) {
Text(retryLabel) Text(actualRetryLabel)
} }
} }
} }
@@ -131,11 +137,13 @@ fun ErrorState(
@Composable @Composable
fun FeedEmptyState( fun FeedEmptyState(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String = "Feed is empty", title: String? = null,
onRefresh: (() -> Unit)? = null, onRefresh: (() -> Unit)? = null,
) { ) {
val actualTitle = title ?: stringResource(SharedRes.strings.feed_empty)
EmptyState( EmptyState(
title = title, title = actualTitle,
modifier = modifier, modifier = modifier,
onRefresh = onRefresh, onRefresh = onRefresh,
) )
@@ -150,8 +158,10 @@ fun FeedErrorState(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null, onRetry: (() -> Unit)? = null,
) { ) {
val formattedMessage = stringResource(SharedRes.strings.error_loading_feed).format(errorMessage)
ErrorState( ErrorState(
message = "Error loading feed: $errorMessage", message = formattedMessage,
modifier = modifier, modifier = modifier,
onRetry = onRetry, onRetry = onRetry,
) )
@@ -28,6 +28,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes
import dev.icerock.moko.resources.compose.stringResource
/** /**
* Generic placeholder screen with title and description. * Generic placeholder screen with title and description.
@@ -58,8 +60,8 @@ fun PlaceholderScreen(
@Composable @Composable
fun SearchPlaceholder(modifier: Modifier = Modifier) { fun SearchPlaceholder(modifier: Modifier = Modifier) {
PlaceholderScreen( PlaceholderScreen(
title = "Search", title = stringResource(SharedRes.strings.screen_search_title),
description = "Search for users, notes, and hashtags.", description = stringResource(SharedRes.strings.screen_search_description),
modifier = modifier, modifier = modifier,
) )
} }
@@ -70,8 +72,8 @@ fun SearchPlaceholder(modifier: Modifier = Modifier) {
@Composable @Composable
fun MessagesPlaceholder(modifier: Modifier = Modifier) { fun MessagesPlaceholder(modifier: Modifier = Modifier) {
PlaceholderScreen( PlaceholderScreen(
title = "Messages", title = stringResource(SharedRes.strings.screen_messages_title),
description = "Your encrypted direct messages will appear here.", description = stringResource(SharedRes.strings.screen_messages_description),
modifier = modifier, modifier = modifier,
) )
} }
@@ -82,8 +84,8 @@ fun MessagesPlaceholder(modifier: Modifier = Modifier) {
@Composable @Composable
fun NotificationsPlaceholder(modifier: Modifier = Modifier) { fun NotificationsPlaceholder(modifier: Modifier = Modifier) {
PlaceholderScreen( PlaceholderScreen(
title = "Notifications", title = stringResource(SharedRes.strings.screen_notifications_title),
description = "Mentions, replies, and reactions will appear here.", description = stringResource(SharedRes.strings.screen_notifications_description),
modifier = modifier, modifier = modifier,
) )
} }
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Login & Auth -->
<string name="login_title">Welcome to Amethyst</string>
<string name="login_subtitle">Sign in to your Nostr account</string>
<string name="login_subtitle_desktop">A Nostr client for desktop</string>
<string name="login_card_title">Login with your Nostr key</string>
<string name="login_card_subtitle">Use nsec for full access or npub for read-only</string>
<string name="login_with_key">Login with Key</string>
<string name="login_button">Login</string>
<string name="login_generate_new">Generate New Key</string>
<string name="login_generate_button">Generate New</string>
<string name="login_key_hint">Enter your private key (nsec) or public key (npub)</string>
<string name="login_key_label">nsec or npub</string>
<string name="login_key_placeholder">nsec1... or npub1...</string>
<string name="login_show_key">Show key</string>
<string name="login_hide_key">Hide key</string>
<!-- New Key Warning -->
<string name="new_key_warning_title">IMPORTANT: Save your keys!</string>
<string name="new_key_warning_message">Your secret key (nsec) is the ONLY way to access your account. If you lose it, your account is gone forever. Save it somewhere safe!</string>
<string name="new_key_public_label">Public Key (shareable):</string>
<string name="new_key_secret_label">Secret Key (NEVER share this!):</string>
<string name="new_key_continue_button">I've saved my keys, continue</string>
<!-- Common Actions -->
<string name="action_copy">Copy</string>
<string name="action_paste">Paste</string>
<string name="action_cancel">Cancel</string>
<string name="action_ok">OK</string>
<string name="action_save">Save</string>
<string name="action_delete">Delete</string>
<string name="action_share">Share</string>
<!-- Errors -->
<string name="error_invalid_key">Invalid key format. Please check and try again.</string>
<string name="error_network">Network error. Please check your connection.</string>
<string name="error_generic">An error occurred. Please try again.</string>
<!-- Loading & Empty States -->
<string name="action_refresh">Refresh</string>
<string name="action_try_again">Try Again</string>
<string name="feed_empty">Feed is empty</string>
<string name="error_loading_feed">Error loading feed: %s</string>
<!-- Placeholder Screens -->
<string name="screen_search_title">Search</string>
<string name="screen_search_description">Search for users, notes, and hashtags.</string>
<string name="screen_messages_title">Messages</string>
<string name="screen_messages_description">Your encrypted direct messages will appear here.</string>
<string name="screen_notifications_title">Notifications</string>
<string name="screen_notifications_description">Mentions, replies, and reactions will appear here.</string>
</resources>
@@ -41,6 +41,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes
import dev.icerock.moko.resources.compose.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
/** /**
* Text field for entering Nostr keys (nsec or npub) with visibility toggle. * Text field for entering Nostr keys (nsec or npub) with visibility toggle.
@@ -50,8 +53,8 @@ fun KeyInputField(
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
label: String = "nsec or npub", label: String = stringResource(SharedRes.strings.login_key_label),
placeholder: String = "nsec1... or npub1...", placeholder: String = stringResource(SharedRes.strings.login_key_placeholder),
errorMessage: String? = null, errorMessage: String? = null,
) { ) {
var showKey by remember { mutableStateOf(false) } var showKey by remember { mutableStateOf(false) }
@@ -73,7 +76,7 @@ fun KeyInputField(
IconButton(onClick = { showKey = !showKey }) { IconButton(onClick = { showKey = !showKey }) {
Icon( Icon(
if (showKey) Icons.Default.VisibilityOff else Icons.Default.Visibility, if (showKey) Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = if (showKey) "Hide key" else "Show key", contentDescription = if (showKey) stringResource(SharedRes.strings.login_hide_key) else stringResource(SharedRes.strings.login_show_key),
) )
} }
}, },
@@ -108,3 +111,20 @@ fun SelectableKeyText(
) )
} }
} }
@Preview
@Composable
fun KeyInputFieldPreview() {
KeyInputField(
value = "nsec1example1234567890",
onValueChange = {},
)
}
@Preview
@Composable
fun SelectableKeyTextPreview() {
SelectableKeyText(
key = "npub1example1234567890abcdefghijklmnopqrstuvwxyz1234567890",
)
}
@@ -43,6 +43,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes
import dev.icerock.moko.resources.compose.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
/** /**
* Login card with Nostr key input field and action buttons. * Login card with Nostr key input field and action buttons.
@@ -60,8 +63,8 @@ fun LoginCard(
onGenerateNew: () -> Unit, onGenerateNew: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
cardWidth: Dp = 400.dp, cardWidth: Dp = 400.dp,
title: String = "Login with your Nostr key", title: String = stringResource(SharedRes.strings.login_card_title),
subtitle: String = "Use nsec for full access or npub for read-only", subtitle: String = stringResource(SharedRes.strings.login_card_subtitle),
) { ) {
var keyInput by remember { mutableStateOf("") } var keyInput by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf<String?>(null) } var errorMessage by remember { mutableStateOf<String?>(null) }
@@ -118,16 +121,25 @@ fun LoginCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
enabled = keyInput.isNotBlank(), enabled = keyInput.isNotBlank(),
) { ) {
Text("Login") Text(stringResource(SharedRes.strings.login_button))
} }
OutlinedButton( OutlinedButton(
onClick = onGenerateNew, onClick = onGenerateNew,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) { ) {
Text("Generate New") Text(stringResource(SharedRes.strings.login_generate_button))
} }
} }
} }
} }
} }
@Preview
@Composable
fun LoginCardPreview() {
LoginCard(
onLogin = { Result.success(Unit) },
onGenerateNew = {},
)
}
@@ -36,6 +36,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes
import dev.icerock.moko.resources.compose.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
/** /**
* Warning card displayed after generating a new Nostr key pair. * Warning card displayed after generating a new Nostr key pair.
@@ -66,7 +69,7 @@ fun NewKeyWarningCard(
modifier = Modifier.padding(24.dp), modifier = Modifier.padding(24.dp),
) { ) {
Text( Text(
"IMPORTANT: Save your keys!", stringResource(SharedRes.strings.new_key_warning_title),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = Color.Red, color = Color.Red,
) )
@@ -74,8 +77,7 @@ fun NewKeyWarningCard(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text( Text(
"Your secret key (nsec) is the ONLY way to access your account. " + stringResource(SharedRes.strings.new_key_warning_message),
"If you lose it, your account is gone forever. Save it somewhere safe!",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
@@ -83,7 +85,7 @@ fun NewKeyWarningCard(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text( Text(
"Public Key (shareable):", stringResource(SharedRes.strings.new_key_public_label),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -93,7 +95,7 @@ fun NewKeyWarningCard(
nsec?.let { secretKey -> nsec?.let { secretKey ->
Text( Text(
"Secret Key (NEVER share this!):", stringResource(SharedRes.strings.new_key_secret_label),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = Color.Red, color = Color.Red,
) )
@@ -106,8 +108,18 @@ fun NewKeyWarningCard(
onClick = onContinue, onClick = onContinue,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Text("I've saved my keys, continue") Text(stringResource(SharedRes.strings.new_key_continue_button))
} }
} }
} }
} }
@Preview
@Composable
fun NewKeyWarningCardPreview() {
NewKeyWarningCard(
npub = "npub1example1234567890abcdefghijklmnopqrstuvwxyz",
nsec = "nsec1example1234567890abcdefghijklmnopqrstuvwxyz",
onContinue = {},
)
}
@@ -37,10 +37,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes
import com.vitorpamplona.amethyst.commons.account.AccountManager import com.vitorpamplona.amethyst.commons.account.AccountManager
import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.account.AccountState
import com.vitorpamplona.amethyst.commons.ui.auth.LoginCard import com.vitorpamplona.amethyst.commons.ui.auth.LoginCard
import com.vitorpamplona.amethyst.commons.ui.auth.NewKeyWarningCard import com.vitorpamplona.amethyst.commons.ui.auth.NewKeyWarningCard
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -59,7 +61,7 @@ fun LoginScreen(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Text( Text(
"Welcome to Amethyst", stringResource(SharedRes.strings.login_title),
style = MaterialTheme.typography.headlineLarge, style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
@@ -67,7 +69,7 @@ fun LoginScreen(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( Text(
"A Nostr client for desktop", stringResource(SharedRes.strings.login_subtitle_desktop),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
+4
View File
@@ -40,6 +40,7 @@ lightcompressor-enhanced = "1.6.0"
markdown = "f92ef49c9d" markdown = "f92ef49c9d"
media3 = "1.9.0" media3 = "1.9.0"
mockk = "1.14.7" mockk = "1.14.7"
mokoResources = "0.25.2"
kotlinx-coroutines-test = "1.10.2" kotlinx-coroutines-test = "1.10.2"
navigationCompose = "2.9.6" navigationCompose = "2.9.6"
okhttp = "5.3.2" okhttp = "5.3.2"
@@ -135,6 +136,8 @@ markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "ric
markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" } markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" } mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" }
moko-resources = { group = "dev.icerock.moko", name = "resources", version.ref = "mokoResources" }
moko-resources-compose = { group = "dev.icerock.moko", name = "resources-compose", version.ref = "mokoResources" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"} kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"}
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", version.ref = "okhttp" } okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", version.ref = "okhttp" }
@@ -173,3 +176,4 @@ androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.lib
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version = "0.6.1" } stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version = "0.6.1" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
mokoResources = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "mokoResources" }