mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 03:45:21 +00:00
mint info screen, wip
This commit is contained in:
@@ -2,6 +2,14 @@ package com.bitchat.android.wallet.data
|
||||
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Represents contact information for a mint
|
||||
*/
|
||||
data class ContactInfo(
|
||||
val method: String, // e.g., "email", "twitter", "nostr"
|
||||
val info: String // e.g., "contact@example.com", "@username", "npub..."
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents a Cashu mint information
|
||||
*/
|
||||
@@ -10,9 +18,9 @@ data class MintInfo(
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
val descriptionLong: String? = null,
|
||||
val contact: String? = null,
|
||||
val contact: List<ContactInfo>? = null,
|
||||
val version: String,
|
||||
val nuts: Map<String, String>, // Simplified - was Map<String, Any>
|
||||
val nuts: Map<String, Any>? = null, // NUT specifications supported by the mint
|
||||
val motd: String? = null,
|
||||
val icon: String? = null,
|
||||
val time: Date? = null
|
||||
|
||||
@@ -21,7 +21,6 @@ class CashuService {
|
||||
private var currentMintUrl: String? = null
|
||||
private var currentUnit: String = "sat"
|
||||
private var isInitialized = false
|
||||
private var isCdkAvailable = false
|
||||
private var repository: WalletRepository? = null
|
||||
|
||||
companion object {
|
||||
@@ -160,38 +159,6 @@ class CashuService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if CDK is available and initialize appropriately
|
||||
*/
|
||||
private fun initializeCdkAvailability(): Boolean {
|
||||
if (isCdkAvailable) return true
|
||||
|
||||
Log.d(TAG, "=== CDK LIBRARY AVAILABILITY CHECK ===")
|
||||
|
||||
try {
|
||||
// Test CDK availability by calling a simple function
|
||||
val testMnemonic = generateMnemonic()
|
||||
Log.d(TAG, "✅ CDK library is fully available and functional!")
|
||||
Log.d(TAG, "Generated test mnemonic with ${testMnemonic.split(" ").size} words")
|
||||
isCdkAvailable = true
|
||||
return true
|
||||
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
Log.w(TAG, "❌ CDK library not available: ${e.message}")
|
||||
} catch (e: FfiException) {
|
||||
Log.w(TAG, "❌ CDK FFI error: ${e.message}")
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Log.w(TAG, "❌ CDK classes not found: ${e.message}")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "❌ Unexpected CDK error: ${e.message}")
|
||||
}
|
||||
|
||||
isCdkAvailable = false
|
||||
Log.w(TAG, "⚠️ CDK library not available - wallet will not function properly")
|
||||
Log.d(TAG, "=== END CDK LIBRARY CHECK ===")
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the CDK wallet with a mint URL
|
||||
*/
|
||||
@@ -200,11 +167,6 @@ class CashuService {
|
||||
try {
|
||||
Log.d(TAG, "Initializing wallet with mint: $mintUrl")
|
||||
|
||||
// Test CDK availability first
|
||||
if (!initializeCdkAvailability()) {
|
||||
return@withContext Result.failure(Exception("CDK library not available"))
|
||||
}
|
||||
|
||||
// Create database path using application context
|
||||
val context = getApplicationContext()
|
||||
val dbPath = "${context.filesDir}/cashu_wallet.db"
|
||||
@@ -269,7 +231,7 @@ class CashuService {
|
||||
initializeWallet(mintUrl).getOrThrow()
|
||||
}
|
||||
|
||||
val mintInfo = if (isCdkAvailable && wallet != null) {
|
||||
val mintInfo = if (wallet != null) {
|
||||
try {
|
||||
// Get real mint info from CDK
|
||||
val mintInfoData = wallet!!.getMintInfo()
|
||||
@@ -280,7 +242,7 @@ class CashuService {
|
||||
name = extractMintName(mintUrl),
|
||||
description = "Cashu mint via CDK",
|
||||
descriptionLong = "Real Cashu mint connection via CDK-FFI",
|
||||
contact = "",
|
||||
contact = emptyList(),
|
||||
version = "1.0.0",
|
||||
nuts = mapOf(
|
||||
"4" to "true", // NUT-4: Mint quote
|
||||
@@ -318,7 +280,7 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
if (wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
@@ -349,7 +311,7 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
if (wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
@@ -446,7 +408,7 @@ class CashuService {
|
||||
)
|
||||
|
||||
// Now receive the token with the properly initialized wallet
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
if (wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
@@ -476,7 +438,7 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
if (wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
@@ -527,7 +489,7 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
if (wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
@@ -574,7 +536,7 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
if (wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
@@ -619,7 +581,7 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
if (wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
@@ -693,14 +655,6 @@ class CashuService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if real CDK is available
|
||||
*/
|
||||
fun isCdkAvailable(): Boolean {
|
||||
initializeCdkAvailability()
|
||||
return isCdkAvailable
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
private fun getApplicationContext(): Context {
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.bitchat.android.wallet.data.Mint
|
||||
import com.bitchat.android.wallet.viewmodel.WalletViewModel
|
||||
import com.bitchat.android.wallet.ui.mintinfo.MintDetailsScreen
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@@ -41,7 +42,41 @@ fun MintsScreen(
|
||||
val showAddMintDialog by viewModel.showAddMintDialog.observeAsState(false)
|
||||
val isLoading by viewModel.isLoading.observeAsState(false)
|
||||
val errorMessage by viewModel.errorMessage.observeAsState()
|
||||
val showMintDetails by viewModel.showMintDetails.observeAsState(false)
|
||||
val selectedMintUrl by viewModel.selectedMintUrl.observeAsState()
|
||||
|
||||
// Show mint details screen if selected
|
||||
if (showMintDetails && selectedMintUrl != null) {
|
||||
MintDetailsScreen(
|
||||
mintUrl = selectedMintUrl!!,
|
||||
walletViewModel = viewModel,
|
||||
onNavigateBack = { viewModel.hideMintDetails() },
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
// Show normal mints list
|
||||
MintsListContent(
|
||||
viewModel = viewModel,
|
||||
mints = mints,
|
||||
activeMint = activeMint,
|
||||
showAddMintDialog = showAddMintDialog,
|
||||
isLoading = isLoading,
|
||||
errorMessage = errorMessage,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MintsListContent(
|
||||
viewModel: WalletViewModel,
|
||||
mints: List<Mint>,
|
||||
activeMint: String?,
|
||||
showAddMintDialog: Boolean,
|
||||
isLoading: Boolean,
|
||||
errorMessage: String?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
@@ -89,14 +124,14 @@ fun MintsScreen(
|
||||
if (mints.isEmpty()) {
|
||||
EmptyMintsCard(onAddClick = { viewModel.showAddMintDialog() })
|
||||
} else {
|
||||
MintsList(
|
||||
mints = mints,
|
||||
activeMint = activeMint,
|
||||
onMintSelect = { viewModel.setActiveMint(it) },
|
||||
onMintEdit = { mint, newNickname ->
|
||||
viewModel.updateMintNickname(mint, newNickname)
|
||||
}
|
||||
)
|
||||
MintsList(
|
||||
mints = mints,
|
||||
activeMint = activeMint,
|
||||
onMintSelect = { viewModel.setActiveMint(it) },
|
||||
onMintInfo = { mintUrl ->
|
||||
viewModel.showMintDetails(mintUrl)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Error message
|
||||
@@ -123,7 +158,7 @@ private fun MintsList(
|
||||
mints: List<Mint>,
|
||||
activeMint: String?,
|
||||
onMintSelect: (String) -> Unit,
|
||||
onMintEdit: (String, String) -> Unit
|
||||
onMintInfo: (String) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
@@ -133,7 +168,7 @@ private fun MintsList(
|
||||
mint = mint,
|
||||
isActive = mint.url == activeMint,
|
||||
onSelect = { onMintSelect(mint.url) },
|
||||
onEdit = { newNickname -> onMintEdit(mint.url, newNickname) }
|
||||
onInfo = { onMintInfo(mint.url) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -144,9 +179,8 @@ private fun MintItem(
|
||||
mint: Mint,
|
||||
isActive: Boolean,
|
||||
onSelect: () -> Unit,
|
||||
onEdit: (String) -> Unit
|
||||
onInfo: () -> Unit
|
||||
) {
|
||||
var showEditDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
@@ -210,11 +244,11 @@ private fun MintItem(
|
||||
)
|
||||
}
|
||||
|
||||
// Edit button
|
||||
IconButton(onClick = { showEditDialog = true }) {
|
||||
// Info button
|
||||
IconButton(onClick = onInfo) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Edit,
|
||||
contentDescription = "Edit",
|
||||
imageVector = Icons.Filled.Info,
|
||||
contentDescription = "Mint Details",
|
||||
tint = Color.Gray,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
@@ -232,17 +266,7 @@ private fun MintItem(
|
||||
}
|
||||
}
|
||||
|
||||
// Edit dialog
|
||||
if (showEditDialog) {
|
||||
EditMintDialog(
|
||||
currentNickname = mint.nickname,
|
||||
onSave = {
|
||||
onEdit(it)
|
||||
showEditDialog = false
|
||||
},
|
||||
onDismiss = { showEditDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -436,100 +460,7 @@ private fun AddMintDialog(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditMintDialog(
|
||||
currentNickname: String,
|
||||
onSave: (String) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
var nickname by remember { mutableStateOf(currentNickname) }
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Edit Mint Nickname",
|
||||
color = Color.White,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = nickname,
|
||||
onValueChange = { nickname = it },
|
||||
label = {
|
||||
Text(
|
||||
text = "Nickname",
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
},
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Color(0xFF00C851),
|
||||
focusedLabelColor = Color(0xFF00C851),
|
||||
unfocusedTextColor = Color.White,
|
||||
focusedTextColor = Color.White
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Cancel button
|
||||
Button(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
border = androidx.compose.foundation.BorderStroke(
|
||||
1.dp,
|
||||
Color.Gray
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Cancel",
|
||||
color = Color.Gray,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
|
||||
// Save button
|
||||
Button(
|
||||
onClick = { onSave(nickname) },
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF00C851)
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
enabled = nickname.isNotEmpty()
|
||||
) {
|
||||
Text(
|
||||
text = "Save",
|
||||
color = Color.Black,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun ErrorCard(
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.bitchat.android.wallet.ui.mintinfo
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.bitchat.android.wallet.data.Mint
|
||||
|
||||
/**
|
||||
* Action buttons section for mint operations
|
||||
*/
|
||||
@Composable
|
||||
fun MintActionButtons(
|
||||
mint: Mint,
|
||||
onEditNickname: () -> Unit,
|
||||
onCopyUrl: () -> Unit,
|
||||
onDeleteMint: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Section divider and title
|
||||
SectionDivider(title = "ACTIONS")
|
||||
|
||||
// Action buttons container
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Edit nickname action
|
||||
ActionButton(
|
||||
icon = Icons.Filled.Edit,
|
||||
label = "Edit Nickname",
|
||||
onClick = onEditNickname
|
||||
)
|
||||
|
||||
// Copy URL action
|
||||
ActionButton(
|
||||
icon = Icons.Filled.ContentCopy,
|
||||
label = "Copy Mint URL",
|
||||
onClick = onCopyUrl
|
||||
)
|
||||
|
||||
// Delete mint action
|
||||
ActionButton(
|
||||
icon = Icons.Filled.Delete,
|
||||
label = "Delete Mint",
|
||||
onClick = onDeleteMint,
|
||||
isDestructive = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionButton(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
isDestructive: Boolean = false,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val iconColor = if (isDestructive) Color(0xFFFF453A) else Color.Gray
|
||||
val textColor = if (isDestructive) Color(0xFFFF453A) else Color.White
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
tint = iconColor,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
color = textColor,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionDivider(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(1.dp)
|
||||
.background(Color(0xFF333333))
|
||||
)
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
color = Color.White,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(1.dp)
|
||||
.background(Color(0xFF333333))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package com.bitchat.android.wallet.ui.mintinfo
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.bitchat.android.wallet.data.Mint
|
||||
|
||||
/**
|
||||
* Dialog for editing mint nickname
|
||||
*/
|
||||
@Composable
|
||||
fun EditMintNicknameDialog(
|
||||
currentNickname: String,
|
||||
onSave: (String) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
var nickname by remember { mutableStateOf(currentNickname) }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp)
|
||||
) {
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Edit Mint Nickname",
|
||||
color = Color.White,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = "Close",
|
||||
tint = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Nickname input
|
||||
OutlinedTextField(
|
||||
value = nickname,
|
||||
onValueChange = { nickname = it },
|
||||
label = {
|
||||
Text(
|
||||
text = "Nickname",
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
},
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Color(0xFF00C851),
|
||||
focusedLabelColor = Color(0xFF00C851),
|
||||
unfocusedTextColor = Color.White,
|
||||
focusedTextColor = Color.White
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Cancel button
|
||||
OutlinedButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.weight(1f),
|
||||
border = androidx.compose.foundation.BorderStroke(
|
||||
1.dp,
|
||||
Color.Gray
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Cancel",
|
||||
color = Color.Gray,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
|
||||
// Save button
|
||||
Button(
|
||||
onClick = { onSave(nickname) },
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF00C851)
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
enabled = nickname.isNotEmpty()
|
||||
) {
|
||||
Text(
|
||||
text = "Save",
|
||||
color = Color.Black,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog for confirming mint deletion
|
||||
*/
|
||||
@Composable
|
||||
fun DeleteMintDialog(
|
||||
mint: Mint,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp)
|
||||
) {
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Delete Mint",
|
||||
color = Color(0xFFFF453A),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = "Close",
|
||||
tint = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Warning message
|
||||
Text(
|
||||
text = "Are you sure you want to delete this mint from your wallet?",
|
||||
color = Color.White,
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
lineHeight = 24.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Mint details
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0x10FFFFFF)
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Nickname
|
||||
MintDetailRow(
|
||||
label = "Nickname:",
|
||||
value = mint.nickname
|
||||
)
|
||||
|
||||
// URL
|
||||
MintDetailRow(
|
||||
label = "URL:",
|
||||
value = mint.url
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Warning note
|
||||
Text(
|
||||
text = "⚠️ This action cannot be undone. Any tokens from this mint will need to be re-added manually.",
|
||||
color = Color(0xFFF18408),
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Cancel button
|
||||
OutlinedButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.weight(1f),
|
||||
border = androidx.compose.foundation.BorderStroke(
|
||||
1.dp,
|
||||
Color.Gray
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Cancel",
|
||||
color = Color.Gray,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
|
||||
// Delete button
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFFF453A)
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Delete",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MintDetailRow(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = Color.Gray,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.width(80.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = value,
|
||||
color = Color.White,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.bitchat.android.wallet.ui.mintinfo
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.bitchat.android.wallet.data.Mint
|
||||
import com.bitchat.android.wallet.viewmodel.WalletViewModel
|
||||
|
||||
/**
|
||||
* Detailed mint information screen with terminal-like design
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MintDetailsScreen(
|
||||
mintUrl: String,
|
||||
walletViewModel: WalletViewModel,
|
||||
onNavigateBack: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val mints by walletViewModel.mints.observeAsState(emptyList())
|
||||
val isLoading by walletViewModel.isLoading.observeAsState(false)
|
||||
val errorMessage by walletViewModel.errorMessage.observeAsState()
|
||||
|
||||
// Find the mint by URL
|
||||
val mint = mints.find { it.url == mintUrl }
|
||||
|
||||
// Show error if mint not found
|
||||
if (mint == null) {
|
||||
MintNotFoundScreen(onNavigateBack = onNavigateBack)
|
||||
return
|
||||
}
|
||||
|
||||
// State for MOTD dismissal
|
||||
var isMotdDismissed by remember { mutableStateOf(false) }
|
||||
|
||||
// State for dialogs
|
||||
var showEditDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
) {
|
||||
// Header with back button
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Mint Details",
|
||||
color = Color(0xFF00C851),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color(0xFF00C851)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Black
|
||||
)
|
||||
)
|
||||
|
||||
// Scrollable content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Mint Header Section
|
||||
MintHeaderSection(mint = mint)
|
||||
|
||||
// MOTD Section
|
||||
if (mint.info?.motd?.isNotEmpty() == true) {
|
||||
MintMotdComponent(
|
||||
message = mint.info.motd,
|
||||
isDismissed = isMotdDismissed,
|
||||
onDismiss = { isMotdDismissed = true }
|
||||
)
|
||||
}
|
||||
|
||||
// Description Section
|
||||
MintDescriptionSection(mint = mint)
|
||||
|
||||
// Contact Section
|
||||
if (mint.info?.contact?.isNotEmpty() == true) {
|
||||
MintContactSection(contacts = mint.info.contact)
|
||||
}
|
||||
|
||||
// Details Section
|
||||
MintDetailsSection(mint = mint)
|
||||
|
||||
// Actions Section
|
||||
MintActionButtons(
|
||||
mint = mint,
|
||||
onEditNickname = { showEditDialog = true },
|
||||
onCopyUrl = { walletViewModel.copyToClipboard(mint.url) },
|
||||
onDeleteMint = { showDeleteDialog = true }
|
||||
)
|
||||
|
||||
// Error display
|
||||
errorMessage?.let { error ->
|
||||
ErrorCard(
|
||||
message = error,
|
||||
onDismiss = { walletViewModel.clearError() }
|
||||
)
|
||||
}
|
||||
|
||||
// Bottom spacing
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// Edit nickname dialog
|
||||
if (showEditDialog) {
|
||||
EditMintNicknameDialog(
|
||||
currentNickname = mint.nickname,
|
||||
onSave = { newNickname ->
|
||||
walletViewModel.updateMintNickname(mint.url, newNickname)
|
||||
showEditDialog = false
|
||||
},
|
||||
onDismiss = { showEditDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
// Delete mint dialog
|
||||
if (showDeleteDialog) {
|
||||
DeleteMintDialog(
|
||||
mint = mint,
|
||||
onConfirm = {
|
||||
walletViewModel.deleteMint(mint.url)
|
||||
showDeleteDialog = false
|
||||
onNavigateBack()
|
||||
},
|
||||
onDismiss = { showDeleteDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
// Loading overlay
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = Color(0xFF00C851)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MintNotFoundScreen(
|
||||
onNavigateBack: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Warning,
|
||||
contentDescription = "Error",
|
||||
tint = Color.Red,
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Mint Not Found",
|
||||
color = Color.White,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "The requested mint could not be found",
|
||||
color = Color.Gray,
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Button(
|
||||
onClick = onNavigateBack,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF00C851)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Go Back",
|
||||
color = Color.Black,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorCard(
|
||||
message: String,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0x30FF0000)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Warning,
|
||||
contentDescription = "Error",
|
||||
tint = Color.Red,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Text(
|
||||
text = message,
|
||||
color = Color.Red,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = "Close",
|
||||
tint = Color.Red,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
package com.bitchat.android.wallet.ui.mintinfo
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
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.sp
|
||||
import com.bitchat.android.wallet.data.ContactInfo
|
||||
import com.bitchat.android.wallet.data.Mint
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Header section showing mint icon and name
|
||||
*/
|
||||
@Composable
|
||||
fun MintHeaderSection(
|
||||
mint: Mint,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1A1A1A).copy(alpha = 0.05f) // Very subtle background
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Mint icon/avatar
|
||||
Card(
|
||||
modifier = Modifier.size(56.dp),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF2A2A2A)
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// For now, we'll use the AccountBalance icon as default
|
||||
// In the future, could load from mint.info?.iconUrl
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AccountBalance,
|
||||
contentDescription = "Mint Icon",
|
||||
tint = Color(0xFF00C851),
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Mint name from info or fallback to nickname
|
||||
Text(
|
||||
text = mint.info?.name?.takeIf { it.isNotEmpty() } ?: mint.nickname,
|
||||
color = Color.White,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
// Show nickname separately if different from name
|
||||
if (mint.info?.name?.isNotEmpty() == true && mint.info.name != mint.nickname) {
|
||||
Text(
|
||||
text = "\"${mint.nickname}\"",
|
||||
color = Color.Gray,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Description section showing mint description and long description
|
||||
*/
|
||||
@Composable
|
||||
fun MintDescriptionSection(
|
||||
mint: Mint,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val info = mint.info
|
||||
if (info?.description.isNullOrEmpty() && info?.descriptionLong.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Short description
|
||||
info?.description?.takeIf { it.isNotEmpty() }?.let { description ->
|
||||
Text(
|
||||
text = description,
|
||||
color = Color.White,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
lineHeight = 24.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
|
||||
// Long description
|
||||
info?.descriptionLong?.takeIf { it.isNotEmpty() }?.let { longDescription ->
|
||||
Text(
|
||||
text = longDescription,
|
||||
color = Color.Gray,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact section showing mint contact information
|
||||
*/
|
||||
@Composable
|
||||
fun MintContactSection(
|
||||
contacts: List<ContactInfo>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (contacts.isEmpty()) return
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Section divider and title
|
||||
SectionDivider(title = "CONTACT")
|
||||
|
||||
// Contact items
|
||||
contacts.forEach { contact ->
|
||||
ContactItem(contact = contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Details section showing mint technical information
|
||||
*/
|
||||
@Composable
|
||||
fun MintDetailsSection(
|
||||
mint: Mint,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var showAllNuts by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Section divider and title
|
||||
SectionDivider(title = "DETAILS")
|
||||
|
||||
// URL
|
||||
DetailItem(
|
||||
icon = Icons.Filled.Link,
|
||||
label = "URL",
|
||||
value = mint.url,
|
||||
onClick = { /* Copy URL handled by parent */ }
|
||||
)
|
||||
|
||||
// NUTs (Cashu specifications)
|
||||
mint.info?.nuts?.let { nuts ->
|
||||
val supportedNuts = nuts.filter { (_, nutInfo) ->
|
||||
when (nutInfo) {
|
||||
is Map<*, *> -> nutInfo["supported"] == true || nutInfo["disabled"] != true
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
if (supportedNuts.isNotEmpty()) {
|
||||
DetailItem(
|
||||
icon = Icons.Filled.Extension,
|
||||
label = "NUTs",
|
||||
value = if (showAllNuts) "Hide" else "Show all",
|
||||
onClick = { showAllNuts = !showAllNuts }
|
||||
)
|
||||
|
||||
if (showAllNuts) {
|
||||
NutsExpandedSection(nuts = supportedNuts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Version
|
||||
mint.info?.version?.takeIf { it.isNotEmpty() }?.let { version ->
|
||||
DetailItem(
|
||||
icon = Icons.Filled.Info,
|
||||
label = "Version",
|
||||
value = version
|
||||
)
|
||||
}
|
||||
|
||||
// Date added
|
||||
DetailItem(
|
||||
icon = Icons.Filled.CalendarToday,
|
||||
label = "Added",
|
||||
value = formatDate(mint.dateAdded)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactItem(
|
||||
contact: ContactInfo,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
clipboardManager.setText(AnnotatedString(contact.info))
|
||||
}
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Contact method icon
|
||||
val icon = when (contact.method.lowercase()) {
|
||||
"email" -> Icons.Filled.Email
|
||||
"twitter", "x" -> Icons.Filled.Share
|
||||
"nostr" -> Icons.Filled.Public
|
||||
else -> Icons.Filled.ContactMail
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contact.method,
|
||||
tint = Color.Gray,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Text(
|
||||
text = contact.info,
|
||||
color = Color.White,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.weight(1f),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ContentCopy,
|
||||
contentDescription = "Copy",
|
||||
tint = Color.Gray,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailItem(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
value: String,
|
||||
onClick: (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (onClick != null) Modifier.clickable { onClick() }
|
||||
else Modifier
|
||||
)
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
tint = Color.Gray,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
color = Color.Gray,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Text(
|
||||
text = value,
|
||||
color = Color.White,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Right,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth(0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NutsExpandedSection(
|
||||
nuts: Map<String, Any>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val nutNames = mapOf(
|
||||
"7" to "Token state check",
|
||||
"8" to "Overpaid Lightning fees",
|
||||
"9" to "Signature restore",
|
||||
"10" to "Spending conditions",
|
||||
"11" to "Pay-To-Pubkey (P2PK)",
|
||||
"12" to "DLEQ proofs",
|
||||
"13" to "Deterministic secrets",
|
||||
"14" to "Hashed Timelock Contracts",
|
||||
"15" to "Partial multi-path payments",
|
||||
"16" to "Animated QR codes",
|
||||
"17" to "WebSocket subscriptions",
|
||||
"18" to "Payment requests",
|
||||
"19" to "Cached Responses",
|
||||
"20" to "Signature on Mint Quote",
|
||||
"21" to "Clear authentication",
|
||||
"22" to "Blind authentication"
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 32.dp), // Indent under the main detail item
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
nuts.filter { (nutNumber, _) ->
|
||||
nutNumber.toIntOrNull()?.let { it >= 7 } == true // Only show NUT-7 and above
|
||||
}.forEach { (nutNumber, _) ->
|
||||
val nutName = nutNames[nutNumber] ?: "Unknown NUT"
|
||||
|
||||
Card(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF2A2A2A)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "$nutNumber:",
|
||||
color = Color.Gray,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = nutName,
|
||||
color = Color.White,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionDivider(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(1.dp)
|
||||
.background(Color(0xFF333333))
|
||||
)
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
color = Color.White,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(1.dp)
|
||||
.background(Color(0xFF333333))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function
|
||||
private fun formatDate(date: Date): String {
|
||||
return SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(date)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.bitchat.android.wallet.ui.mintinfo
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* Component for displaying mint MOTD (Message of the Day) with dismiss functionality
|
||||
*/
|
||||
@Composable
|
||||
fun MintMotdComponent(
|
||||
message: String,
|
||||
isDismissed: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (message.isEmpty()) return
|
||||
|
||||
if (!isDismissed) {
|
||||
// Active MOTD card
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = false) { }, // Prevent accidental clicks
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color.Black
|
||||
),
|
||||
border = BorderStroke(1.dp, Color(0xFFF18408)) // Orange border like in Vue version
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
// Info icon
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Info,
|
||||
contentDescription = "MOTD",
|
||||
tint = Color(0xFFF18408),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
// Content
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
// Header with title and close button
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Message of the Day",
|
||||
color = Color(0xFFF18408),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = "Dismiss",
|
||||
tint = Color(0xFFF18408),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
// MOTD message
|
||||
Text(
|
||||
text = message,
|
||||
color = Color(0xFFF18408),
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 16.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Dismissed MOTD - minimal display
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { /* Could expand back if needed */ },
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1A1A1A)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Info,
|
||||
contentDescription = "MOTD",
|
||||
tint = Color.Gray,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = "Message of the Day",
|
||||
color = Color.Gray,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
Text(
|
||||
text = message,
|
||||
color = Color.Gray,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 16.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
maxLines = 2 // Truncate in dismissed state
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,6 +271,43 @@ class MintManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a mint from the repository
|
||||
*/
|
||||
fun deleteMint(mintUrl: String) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val currentMints = _mints.value ?: emptyList()
|
||||
val updatedMints = currentMints.filter { it.url != mintUrl }
|
||||
|
||||
// Save updated mints list (remove the mint)
|
||||
repository.clearAllData()
|
||||
updatedMints.forEach { mint ->
|
||||
repository.saveMint(mint).onFailure { error ->
|
||||
Log.e(TAG, "Failed to save mint during deletion: ${mint.url}", error)
|
||||
}
|
||||
}
|
||||
|
||||
// If the deleted mint was active, clear active mint
|
||||
if (_activeMint.value == mintUrl) {
|
||||
repository.setActiveMint("")
|
||||
_activeMint.value = null
|
||||
}
|
||||
|
||||
// Reload mints
|
||||
repository.getMints().onSuccess { mintList ->
|
||||
_mints.value = mintList
|
||||
}
|
||||
|
||||
Log.d(TAG, "Successfully deleted mint: $mintUrl")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to delete mint: $mintUrl", e)
|
||||
_errorMessage.value = "Failed to delete mint: ${e.message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear error message
|
||||
*/
|
||||
|
||||
@@ -24,6 +24,12 @@ class UIStateManager {
|
||||
private val _showAddMintDialog = MutableLiveData<Boolean>(false)
|
||||
val showAddMintDialog: androidx.lifecycle.LiveData<Boolean> = _showAddMintDialog
|
||||
|
||||
private val _showMintDetails = MutableLiveData<Boolean>(false)
|
||||
val showMintDetails: androidx.lifecycle.LiveData<Boolean> = _showMintDetails
|
||||
|
||||
private val _selectedMintUrl = MutableLiveData<String?>(null)
|
||||
val selectedMintUrl: androidx.lifecycle.LiveData<String?> = _selectedMintUrl
|
||||
|
||||
// Success animation state
|
||||
private val _showSuccessAnimation = MutableLiveData<Boolean>(false)
|
||||
val showSuccessAnimation: androidx.lifecycle.LiveData<Boolean> = _showSuccessAnimation
|
||||
@@ -84,6 +90,16 @@ class UIStateManager {
|
||||
_showAddMintDialog.value = false
|
||||
}
|
||||
|
||||
fun showMintDetails(mintUrl: String) {
|
||||
_selectedMintUrl.value = mintUrl
|
||||
_showMintDetails.value = true
|
||||
}
|
||||
|
||||
fun hideMintDetails() {
|
||||
_showMintDetails.value = false
|
||||
_selectedMintUrl.value = null
|
||||
}
|
||||
|
||||
// Animation Management
|
||||
|
||||
fun showSuccessAnimation(animationData: WalletViewModel.SuccessAnimationData) {
|
||||
|
||||
@@ -58,6 +58,8 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
val sendType: LiveData<SendType> = uiStateManager.sendType
|
||||
val receiveType: LiveData<ReceiveType> = uiStateManager.receiveType
|
||||
val showAddMintDialog: LiveData<Boolean> = uiStateManager.showAddMintDialog
|
||||
val showMintDetails: LiveData<Boolean> = uiStateManager.showMintDetails
|
||||
val selectedMintUrl: LiveData<String?> = uiStateManager.selectedMintUrl
|
||||
val showSuccessAnimation: LiveData<Boolean> = uiStateManager.showSuccessAnimation
|
||||
val successAnimationData: LiveData<SuccessAnimationData?> = uiStateManager.successAnimationData
|
||||
val showFailureAnimation: LiveData<Boolean> = uiStateManager.showFailureAnimation
|
||||
@@ -182,6 +184,20 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
|
||||
fun clearError() = uiStateManager.clearError()
|
||||
|
||||
// Mint details navigation
|
||||
fun showMintDetails(mintUrl: String) = uiStateManager.showMintDetails(mintUrl)
|
||||
|
||||
fun hideMintDetails() = uiStateManager.hideMintDetails()
|
||||
|
||||
// Copy to clipboard functionality
|
||||
fun copyToClipboard(text: String) {
|
||||
// This will be handled by the UI components directly using LocalClipboardManager
|
||||
// We can add a success message here if needed
|
||||
}
|
||||
|
||||
// Delete mint functionality
|
||||
fun deleteMint(mintUrl: String) = mintManager.deleteMint(mintUrl)
|
||||
|
||||
// Token input management - Delegate to TokenManager
|
||||
fun setTokenInput(token: String) = tokenManager.setTokenInput(token)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user