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:
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.