app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit c72a52d0fb61f72578fa1ca0b6a8d9675d0e30d7
parent c0d2e3970a1e69b1f6dfb4f4a98540a906af5108
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 20:00:01 +0000

ui(home): add minimal active account screen

- render the active Nostr identity and cached profile metadata
- expose relay profile and configured-relay runtime state
- forward refresh and sign-out actions through the thin store
- verify required home content and controls with Compose tests

Diffstat:
Mapp/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/StudioScreen.kt | 57++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mapp/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt | 2++
Mapp/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/StudioScreenTest.kt | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mdocs/implementation/nostr-runtime-rcld.md | 2+-
4 files changed, 106 insertions(+), 2 deletions(-)

diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/StudioScreen.kt b/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/StudioScreen.kt @@ -40,6 +40,8 @@ data class StudioUiActions( val requestAccountRemoval: (String) -> Unit = {}, val cancelAccountRemoval: () -> Unit = {}, val confirmAccountRemoval: () -> Unit = {}, + val refreshActiveProfile: () -> Unit = {}, + val signOut: () -> Unit = {}, ) @Composable @@ -47,7 +49,60 @@ fun StudioScreen( model: StudioUiModel, actions: StudioUiActions, ) { - InactiveAccountsScreen(model, actions) + if (model.session == org.radroots.studio.ffi.SessionStateDto.ACTIVE && model.activeAccount != null) { + ActiveAccountHome(model, model.activeAccount, actions) + } else { + InactiveAccountsScreen(model, actions) + } +} + +@Composable +private fun ActiveAccountHome( + model: StudioUiModel, + active: ActiveAccountUiModel, + actions: StudioUiActions, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(WindowBackgroundColor) + .padding(24.dp) + .testTag("home-screen"), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + BasicText("radroots") + BasicText(active.heading) + BasicText(active.account.npub, Modifier.testTag("active-npub")) + BasicText(active.account.publicKeyHex, Modifier.testTag("active-pubkey-hex")) + BasicText("Name: ${active.profile.name}", Modifier.testTag("active-profile-name")) + BasicText("Display name: ${active.profile.displayName}") + BasicText("NIP-05: ${active.profile.nip05}") + BasicText("About: ${active.profile.about}", Modifier.testTag("active-profile-about")) + BasicText("Picture: ${active.profile.picture}") + BasicText("Relay: ${active.relayState}", Modifier.testTag("relay-state")) + BasicText("Profile: ${active.profileState}", Modifier.testTag("profile-state")) + BasicText("Configured relays") + if (model.configuredRelays.isEmpty()) { + BasicText("None") + } else { + model.configuredRelays.forEach { relay -> BasicText(relay) } + } + TextAction( + text = "Refresh metadata", + testTag = "refresh-profile", + contentDescription = "Refresh active Nostr profile metadata", + enabled = !model.busy, + onClick = actions.refreshActiveProfile, + ) + TextAction( + text = "Sign out", + testTag = "sign-out", + contentDescription = "Sign out of the active account", + enabled = !model.busy, + onClick = actions.signOut, + ) + model.problem?.let { BasicText(it, Modifier.testTag("home-problem")) } + } } @Composable diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt b/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt @@ -40,6 +40,8 @@ fun RadrootsApplication( requestAccountRemoval = store::requestAccountRemoval, cancelAccountRemoval = store::cancelAccountRemoval, confirmAccountRemoval = store::confirmAccountRemoval, + refreshActiveProfile = store::refreshActiveProfile, + signOut = store::signOut, ), ) } diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/StudioScreenTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/StudioScreenTest.kt @@ -127,6 +127,53 @@ class StudioScreenTest { onNodeWithTag("remove-confirm", useUnmergedTree = true).performClick() assertEquals(1, confirmations) } + + @Test + fun activeHomeShowsIdentityProfileRelayAndCommands() = runComposeUiTest { + var refreshCalls = 0 + var signOutCalls = 0 + val account = accountUi("33".repeat(32), selected = true) + val active = ActiveAccountUiModel( + account = account, + heading = "Alice", + relayState = "connected", + profileState = "fresh", + profile = ProfileUiModel( + name = "alice", + displayName = "Alice", + nip05 = "alice@example.com", + about = "Local grower", + picture = "https://example.com/alice.png", + ), + ) + setContent { + StudioScreen( + model = emptyUiModel().copy( + accounts = listOf(account), + activeAccount = active, + configuredRelays = listOf("ws://localhost:8080"), + session = SessionStateDto.ACTIVE, + ), + actions = StudioUiActions( + refreshActiveProfile = { refreshCalls += 1 }, + signOut = { signOutCalls += 1 }, + ), + ) + } + + onNodeWithTag("home-screen").assertIsDisplayed() + onNodeWithTag("active-npub").assertIsDisplayed() + onNodeWithTag("active-pubkey-hex").assertIsDisplayed() + onNodeWithTag("active-profile-name").assertIsDisplayed() + onNodeWithTag("active-profile-about").assertIsDisplayed() + onNodeWithTag("relay-state").assertIsDisplayed() + onNodeWithTag("profile-state").assertIsDisplayed() + onNodeWithText("ws://localhost:8080").assertIsDisplayed() + onNodeWithTag("refresh-profile").performClick() + onNodeWithTag("sign-out").performClick() + assertEquals(1, refreshCalls) + assertEquals(1, signOutCalls) + } } private fun emptyUiModel( diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -537,7 +537,7 @@ handoff commit sequence. - [x] 51. Implement inactive account screen generate/import controls. - [x] 52. Implement generated-key backup panel. - [x] 53. Implement saved-account list, activation, and removal controls. -- [ ] 54. Implement active account home screen. +- [x] 54. Implement active account home screen. - [ ] 55. Implement switch account flow in UI. - [ ] 56. Remove canonical Kotlin account reducer/store usage. - [ ] 57. Remove generic server-account remnants.