bugfixes
This commit is contained in:
+1
-1
@@ -200,7 +200,7 @@ class EventCollectionState<T : Any>(
|
||||
// Sort if comparator provided, otherwise keep newest first (pending items already at end)
|
||||
val sorted =
|
||||
if (sortComparator != null) {
|
||||
merged.sortedWith(sortComparator)
|
||||
merged.sortedWith(sortComparator).distinctBy { getId(it) }
|
||||
} else {
|
||||
// Reverse so newest (pending) items come first
|
||||
(pendingItems.reversed() + _items.value).distinctBy { getId(it) }
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop
|
||||
|
||||
/**
|
||||
* Debug configuration for the desktop application.
|
||||
*
|
||||
* To enable debug mode, set the environment variable:
|
||||
* AMETHYST_DEBUG=true
|
||||
*
|
||||
* Or run with:
|
||||
* ./gradlew :desktopApp:run -PamethystDebug=true
|
||||
*/
|
||||
object DebugConfig {
|
||||
/**
|
||||
* Enables developer settings and debug features.
|
||||
*
|
||||
* When true, shows:
|
||||
* - Raw nsec/npub in settings
|
||||
* - Additional logging
|
||||
* - Debug UI elements
|
||||
*/
|
||||
val isDebugMode: Boolean by lazy {
|
||||
// Check environment variable
|
||||
val envDebug = System.getenv("AMETHYST_DEBUG")?.toBoolean() ?: false
|
||||
|
||||
// Check system property (for gradle -PamethystDebug=true)
|
||||
val propDebug = System.getProperty("amethyst.debug")?.toBoolean() ?: false
|
||||
|
||||
envDebug || propDebug
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug information.
|
||||
*/
|
||||
fun log(message: String) {
|
||||
if (isDebugMode) {
|
||||
println("[DEBUG] $message")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,21 +91,21 @@ private val isMacOS = System.getProperty("os.name").lowercase().contains("mac")
|
||||
* Desktop navigation state - extends AppScreen with dynamic destinations.
|
||||
*/
|
||||
sealed class DesktopScreen {
|
||||
data object Feed : DesktopScreen()
|
||||
object Feed : DesktopScreen()
|
||||
|
||||
data object Search : DesktopScreen()
|
||||
object Search : DesktopScreen()
|
||||
|
||||
data object Messages : DesktopScreen()
|
||||
object Messages : DesktopScreen()
|
||||
|
||||
data object Notifications : DesktopScreen()
|
||||
object Notifications : DesktopScreen()
|
||||
|
||||
data object MyProfile : DesktopScreen()
|
||||
object MyProfile : DesktopScreen()
|
||||
|
||||
data class UserProfile(
|
||||
val pubKeyHex: String,
|
||||
) : DesktopScreen()
|
||||
|
||||
data object Settings : DesktopScreen()
|
||||
object Settings : DesktopScreen()
|
||||
}
|
||||
|
||||
fun main() =
|
||||
@@ -365,7 +365,7 @@ fun MainContent(
|
||||
onScreenChange(DesktopScreen.UserProfile(pubKeyHex))
|
||||
},
|
||||
)
|
||||
DesktopScreen.Settings -> RelaySettingsScreen(relayManager)
|
||||
DesktopScreen.Settings -> RelaySettingsScreen(relayManager, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,17 +405,37 @@ fun ProfileScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RelaySettingsScreen(relayManager: DesktopRelayConnectionManager) {
|
||||
fun RelaySettingsScreen(
|
||||
relayManager: DesktopRelayConnectionManager,
|
||||
account: AccountState.LoggedIn,
|
||||
) {
|
||||
val relayStatuses by relayManager.relayStatuses.collectAsState()
|
||||
val connectedRelays by relayManager.connectedRelays.collectAsState()
|
||||
var newRelayUrl by remember { mutableStateOf("") }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Text(
|
||||
"Relay Settings",
|
||||
"Settings",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
// Developer Settings Section (only in debug mode)
|
||||
if (DebugConfig.isDebugMode) {
|
||||
com.vitorpamplona.amethyst.desktop.ui
|
||||
.DevSettingsSection(account = account)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
"Relay Settings",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
@@ -470,7 +490,7 @@ fun RelaySettingsScreen(relayManager: DesktopRelayConnectionManager) {
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
items(relayStatuses.values.toList()) { status ->
|
||||
items(relayStatuses.values.toList(), key = { it.url.url }) { status ->
|
||||
RelayStatusCard(
|
||||
status = status,
|
||||
onRemove = { relayManager.removeRelay(status.url) },
|
||||
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.account.AccountState
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.StringSelection
|
||||
|
||||
/**
|
||||
* Developer settings section - shows sensitive keys for debugging.
|
||||
* Only enable in debug builds or with explicit debug flag.
|
||||
*/
|
||||
@Composable
|
||||
fun DevSettingsSection(
|
||||
account: AccountState.LoggedIn,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showKeys by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = Color(0xFFFF9800), // Orange warning color
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
).background(
|
||||
color = Color(0xFF332200), // Dark orange tint
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
).padding(16.dp),
|
||||
) {
|
||||
// Warning header
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
contentDescription = "Warning",
|
||||
tint = Color(0xFFFF9800),
|
||||
)
|
||||
Text(
|
||||
"Developer Settings",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFFFF9800),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
"Debug mode only - Handle keys with care",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
HorizontalDivider(color = Color(0xFFFF9800).copy(alpha = 0.3f))
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Toggle button
|
||||
OutlinedButton(
|
||||
onClick = { showKeys = !showKeys },
|
||||
colors =
|
||||
ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = Color(0xFFFF9800),
|
||||
),
|
||||
) {
|
||||
Text(if (showKeys) "Hide Keys" else "Show Keys")
|
||||
}
|
||||
|
||||
if (showKeys) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// npub
|
||||
KeyRow(
|
||||
label = "Public Key (npub)",
|
||||
value = account.npub,
|
||||
isSensitive = false,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
// nsec (only if available)
|
||||
account.nsec?.let { nsec ->
|
||||
KeyRow(
|
||||
label = "Private Key (nsec)",
|
||||
value = nsec,
|
||||
isSensitive = true,
|
||||
)
|
||||
} ?: run {
|
||||
Text(
|
||||
"Read-only mode - No private key available",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
// Hex key
|
||||
KeyRow(
|
||||
label = "Public Key (hex)",
|
||||
value = account.pubKeyHex,
|
||||
isSensitive = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KeyRow(
|
||||
label: String,
|
||||
value: String,
|
||||
isSensitive: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var copiedRecently by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color =
|
||||
if (isSensitive) {
|
||||
Color(0xFFFF5252)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
},
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
copyToClipboard(value)
|
||||
copiedRecently = true
|
||||
},
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor =
|
||||
if (copiedRecently) {
|
||||
Color(0xFF4CAF50)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
},
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy",
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
)
|
||||
Text(if (copiedRecently) "Copied!" else "Copy")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
// Display the key in monospace
|
||||
Text(
|
||||
value,
|
||||
style =
|
||||
MaterialTheme.typography.bodySmall.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
),
|
||||
color =
|
||||
if (isSensitive) {
|
||||
Color(0xFFFF5252)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = Color(0xFF1A1A1A),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
).padding(8.dp),
|
||||
)
|
||||
|
||||
// Reset copied state after a delay
|
||||
if (copiedRecently) {
|
||||
androidx.compose.runtime.LaunchedEffect(Unit) {
|
||||
kotlinx.coroutines.delay(2000)
|
||||
copiedRecently = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text to system clipboard using AWT Toolkit.
|
||||
*/
|
||||
private fun copyToClipboard(text: String) {
|
||||
try {
|
||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||
val selection = StringSelection(text)
|
||||
clipboard.setContents(selection, selection)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DebugConfigTest {
|
||||
@Test
|
||||
fun testDebugModeCanBeQueried() {
|
||||
// This test verifies that DebugConfig.isDebugMode is accessible
|
||||
// The actual value depends on environment/system properties
|
||||
val debugMode = DebugConfig.isDebugMode
|
||||
// Just verify it returns a boolean (true or false)
|
||||
assertTrue(debugMode || !debugMode, "isDebugMode should return a boolean value")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDebugLogDoesNotThrow() {
|
||||
// Verify debug logging doesn't throw exceptions
|
||||
try {
|
||||
DebugConfig.log("Test debug message")
|
||||
assertTrue(true)
|
||||
} catch (e: Exception) {
|
||||
assertFalse(true, "Debug log should not throw exceptions: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDebugConfigIsObject() {
|
||||
// Verify DebugConfig is a singleton object
|
||||
val instance1 = DebugConfig
|
||||
val instance2 = DebugConfig
|
||||
assertTrue(instance1 === instance2, "DebugConfig should be a singleton object")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user